HTML5 Canvas入门:第2部分(示例)






4.62/5 (6投票s)
本文将带您完整了解如何在 Canvas 上创建图形。
引言
本文是我上一篇文章《HTML5 <Canvas> 入门:第一部分》(也发布在 CodeProject 上)的续篇,展示了使用 Canvas 进行一些静态 2D 绘图的演练示例(关于动画入门,请等待我的下一篇文章)。这是我的“HTML5 <Canvas>
”系列文章的第二篇。
在上一篇文章中,我们了解了 <Canvas>
的 context 如何提供给我们一个 API,其中包含一组 方法和属性,用于在其表面上进行绘制。现在,在这篇文章中,我们将使用其中一些方法来绘制我最喜欢的游戏 PORTAL2 的 Logo。

上面是在 Canvas 上绘制的实际完成的艺术品,而 下面我们将看到我们如何一点一点地绘制这件艺术品。并且请注意,本文仅用于介绍 Canvas 提供的各种方法的使用,可能不会展示最佳且高效的绘图方式。因此,考虑到这一点,让我们开始吧。
首先
您可能已经知道,在进行任何绘制之前,我们需要获取对 <Canvas> 元素的引用以进行操作,而这个引用就是 <Canvas> 的 context
对象。 <Canvas> 元素的 context
为我们提供了所有这些方法 API,用于在 <Canvas>
上进行绘制和操作。要获取 context
,我们使用 “getContext()
” 方法,并将字符串 “2d”
作为参数*。此外,由于我们更习惯使用“度”来指定角度,而 Canvas context
的所有函数都接受顺时针的“弧度”作为参数,我们将使用以下函数将角度(度)转换为逆时针*弧度。另请注意,建议使用 context
的 “save()
” 和 “restore()
” 方法,在进行任何操作和变换之前将 context
的当前状态保存在堆栈上,然后分别恢复到其以前的状态。
(有关详细信息,请参阅我之前的文章或 w3c 草案)。
//context of the canvas
var ctx = document.getElementById("portalcanvas").getContext("2d");
//function to convert radians to degrees
var acDegToRad = function(deg){
return deg* (-(Math.PI / 180.0));
}
绘制“2”
我们将从在 Canvas 上绘制数字“2
”开始。它包含 3 个部分 - 底座矩形、倾斜矩形和一个弧形。
-
2 的底座:底座矩形是最容易绘制的,只需将
context
的 “fillStyle
” 属性设置为浅灰色,然后使用 “fillRect( x, y, width, height)
” 方法绘制矩形。ctx.save(); ctx.fillStyle = "rgb(110,110,110)"; ctx.fillRect(20,200,120,35); ctx.restore();
如果我们愿意,也可以将上述矩形绘制成一条较短的水平线,其线宽等于上述矩形的高度。但这将涉及更多步骤,例如创建路径,然后沿路径绘制线条,对于这种基本形状来说更为复杂。
-
2 的倾斜部分:倾斜矩形只是一个简单的矩形,但需要倾斜 35 度。因此,要创建倾斜矩形,首先我们将坐标系的原点(即变换矩阵)平移到 2 的底座矩形的顶部边缘,使用 “
translate( newX, newY)
”,然后以新的原点为轴心/中心,使用 “rotate(radians)
” 方法将坐标系逆时针旋转 35 度,然后简单地使用fillRect( x, y, width, height)
绘制矩形。ctx.save(); ctx.fillStyle = "rgb(110,110,110)"; ctx.translate(20,200); ctx.rotate(acDegToRad(35)); ctx.fillRect(0,0,100,35); ctx.restore();
还要注意,我们在进行任何操作之前如何使用了
save()
,并在绘制完成后使用了restore()
。这可以确保坐标系的平移和旋转不会影响我们之后将要进行的其余绘制。这样,Canvas 的context
状态始终保持在以前的状态,在这种情况下是初始状态。请记住,save()
和restore()
不保存/恢复 Canvas 上的内容,它们只保存/恢复 Canvas 绘图上下文中的属性/特性(如“fillStyle
”、“strokeStyle
”、“lineWidth
”等)和坐标空间的状态。 -
2 的弧形:数字“
2
”的顶部弧形无法通过矩形方法绘制,但可以像一条线弧一样绘制,其线宽等于先前矩形的高度。要创建任何线形,我们首先通过调用 “beginPath()
” 方法来开始一个路径,然后调用任何形状方法,如 “rect()
”、“arc()
”、“lineTo()
” 等,将它们添加到路径中,然后可以选择调用 “closePath()
” 方法来完成路径并开始新的路径。在此步骤中,我们将开始一个新路径,并通过使用 “arc(x,y,radius, startAngle, endAngle)
” 方法将一个弧形添加到路径中。到目前为止,我们只创建了路径,要实际在 Canvas 上绘制弧形,我们将调用 “stroke()
” 方法。但由于 stroke 会使用默认颜色绘制,因此在调用 “stroke()
“ 之前,我们将context
的 “strokeStyle
” 属性设置为浅灰色。ctx.save(); ctx.lineWidth = 35; ctx.beginPath(); ctx.arc(77,135,40,acDegToRad(-40),acDegToRad(180),true); ctx.strokeStyle = "rgb(110,110,110)"; ctx.stroke(); ctx.restore();
绘制“蓝色小人”
接下来要绘制的是从墙壁中伸出的蓝色小人。这幅艺术品由墙壁、蓝色小人的头部、肚子,然后是手和腿组成。
-
墙壁:墙壁只是一个简单、细长且高大的蓝色矩形。
ctx.save(); ctx.fillStyle = "rgb(0,160,212)"; ctx.fillRect(162,20,8,300); ctx.restore();
-
头部:蓝色小人的头部也是一个简单的圆形,但由于我们**没有**直接的 “
fillCircle()
” 方法,我们将需要使用与绘制“2
”的弧形类似的方法。我们将开始一个新路径,添加一个完整的 360 度弧形,并用颜色填充它。对于填充,我们将使用 “fill()
” 方法,并配合将 “fillStyle
” 属性设置为浅蓝色,以蓝色的方式填充路径,从而创建一个填充的圆形。ctx.save(); ctx.fillStyle = "rgba(256,256,256,0.75)"; ctx.fillRect(0,0,300,350); ctx.fillStyle = "rgb(0,160,212)"; ctx.beginPath(); ctx.arc(225,80,35,acDegToRad(0),acDegToRad(360)); ctx.fill(); ctx.restore();
这里需要注意的一点是,当你创建一个路径时,你有两个选择,要么调用 “
stroke()
” 方法使用当前的 “strokeStyle
” 来绘制该路径,就像我们之前做的那样,并且将为手和腿这样做,要么调用 “fill()
” 方法用当前的 “fillStyle
” 来填充该路径,就像我们刚才为头部所做的那样,并将为肚子这样做。我们还有一个选择,就是调用 “clip()
” 方法(稍后讨论)。 -
肚子:蓝色小人的肚子也是通过创建一个三角形路径并用蓝色填充该三角形来绘制的。要创建三角形路径,我们首先开始一个新路径,使用 “
moveTo(x, y)
” 方法将初始绘图点从原点或任何最后一个位置(设为 O)移动到头部下方墙壁上的一个点,该方法将绘图点从当前绘图点移动到一个新点(设为 A),而不将 O 和 A 之间的线添加到路径中,然后使用 “lineTo( x, y)
” 方法将绘图点移动到一个新点(设为 B),并将 A 和 B 之间的线添加到路径中。同样,将第三个点(设为 C)添加到路径中以完成三角形的三个点。这也会将 B 和 C 之间的线添加到路径中。现在,您可以选择使用 “lineTo( x, y)
” 方法回到点 A,并将 C 和 A 之间的线添加到路径中,从而闭合路径,但默认情况下,“fill()
” 将自动假定在开口点和闭合点之间有一条线,并将填充封闭区域。ctx.save(); ctx.fillStyle = "rgb(0,160,212)"; ctx.beginPath(); ctx.moveTo(170,90); //point A ctx.lineTo(230,140); //point B ctx.lineTo(170,210); //point C ctx.fill(); //fill area between ABC ctx.restore();
-
手臂:对于手臂,我们将像上面一样简单地创建两条线。 A 到 B 用于肩部到肘部,B 到 C 用于前臂。但默认情况下,线条的宽度为 1px。首先,我们将
context
的 “lineWidth
” 属性设置为 25px,然后由于线条的末端和连接处(在 B 点)默认是矩形的,我们将 “lineCap
” 属性(用于线条末端)和 “lineJoin
” 属性都设置为 “round
”。而且,由于我们希望绘制线条而不是填充它们之间的空间,我们将调用 context 的 “stroke()
” 方法。ctx.save(); ctx.lineWidth = 25; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = "rgb(0,160,212)"; ctx.beginPath(); ctx.moveTo(222,150); //point A ctx.lineTo(230,190); //point B ctx.lineTo(270,220); //point C ctx.stroke(); ctx.restore();
-
腿部:腿部可以与我们绘制手臂的方式完全相同,因此代码与上面基本相同,只是坐标不同。
ctx.save(); ctx.lineWidth = 25; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = "rgb(0,160,212)"; ctx.moveTo(160,210); //point A ctx.lineTo(195,260); //point B ctx.lineTo(160,290); //point C ctx.stroke(); ctx.restore();
但是上面的代码存在一个问题,结果的腿部看起来不像我们想要的。腿部的一部分隐藏在墙壁中,所以我们需要剪裁掉多余的腿部。
-
剪裁腿部:使用上面的代码,我们没有得到想要的结果,但不用担心,“
clip()
” 方法可以派上用场。Clip
类似于fill
方法,但它不是用某种颜色填充路径所包围的区域,而是创建一个封闭的不可见边界(图中只绘制了三角形的轮廓以突出显示剪裁区域),在此区域内的任何绘制都会被剪裁,而只有位于剪裁区域内的绘制才会显示。因此,为了剪裁穿过墙壁伸出的腿部,我们将首先创建一个剪裁区域,方法是创建一个三角形路径,其中一条边与墙壁重合,然后像绘制手臂一样绘制腿部。ctx.save(); //code for drawing clipping region ctx.beginPath(); ctx.moveTo(170,200); //point A ctx.lineTo(250,260); //point B ctx.lineTo(170,320); //point C ctx.lineTo(170,200); //back to point A to close the path ctx.clip(); //set the above path for clipping region //code for drawing leg ctx.lineWidth = 25; ctx.lineCap = "round"; ctx.strokeStyle = "rgb(0,160,212)"; ctx.lineJoin = "round"; ctx.beginPath(); ctx.moveTo(160,210); ctx.lineTo(195,260); ctx.lineTo(160,290); ctx.stroke(); ctx.restore();
这就是创建 Portal 2 Logo 所需的所有内容。下面是一个动画 Canvas,展示了我们用来绘制它的所有部分(实际上,本文中的所有图像都绘制在 <Canvas>
元素上),并附有我用来绘制它的完整组合代码。所以,我希望您喜欢这篇文章,并请继续关注我的下一篇文章,我将在其中讨论 <Canvas>
上的 2D 动画。
(function(){
var ctx = document.getElementById("portalcanvas").getContext("2d");
//function to convert deg to radian
var acDegToRad = function(deg){
return deg* (-(Math.PI / 180.0));
}
//save the initial state of the context
ctx.save();
//set fill color to gray
ctx.fillStyle = "rgb(110,110,110)";
//save the current state with fillcolor
ctx.save();
//draw 2's base rectangle
ctx.fillRect(20,200,120,35);
//bring origin to 2's base
ctx.translate(20,200);
//rotate the canvas 35 deg anti-clockwise
ctx.rotate(acDegToRad(35));
//draw 2's slant rectangle
ctx.fillRect(0,0,100,35);
//restore the canvas to reset transforms
ctx.restore();
//set stroke color width and draw the 2's top semi circle
ctx.strokeStyle = "rgb(110,110,110)";
ctx.lineWidth = 35;
ctx.beginPath();
ctx.arc(77,135,40,acDegToRad(-40),acDegToRad(180),true);
ctx.stroke();
//reset canvas transforms
ctx.restore();
//change color to blue
ctx.fillStyle = "rgb(0,160,212)";
//save current state of canvas
ctx.save();
//draw long dividing rectangle
ctx.fillRect(162,20,8,300);
//draw player head circle
ctx.beginPath();
ctx.arc(225,80,35,acDegToRad(0),acDegToRad(360));
ctx.fill();
//start new path for tummy :)
ctx.beginPath();
ctx.moveTo(170,90);
ctx.lineTo(230,140);
ctx.lineTo(170,210);
ctx.fill();
//start new path for hand
//set lineCap and lineJoin to "round", blue color
//for stroke, and width of 25px
ctx.lineWidth = 25;
ctx.lineCap = "round";
ctx.strokeStyle = "rgb(0,160,212)";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(222,150);
ctx.lineTo(230,190);
ctx.lineTo(270,220);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(170, 200);
ctx.lineTo(250, 260);
ctx.lineTo(170,320);
ctx.clip();
//begin new path for drawing leg
ctx.beginPath();
ctx.moveTo(160,210);
ctx.lineTo(195,260);
ctx.lineTo(160,290);
ctx.stroke();
//restore the <code class="prettyprint">context</code>
//back to its initial state
ctx.restore();
})()