JavaScript图形实例:Canvas API
1.Canvas概述
Canvas API(画布)用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)。
要使用HTML5在浏览器窗口中绘制图形,首先需要在HTML文档中新建一个canvas网页元素。一般方法如下:
<canvas id="myCanvas" width="400" height="300">
您的浏览器不支持canvas!
</canvas>
上面这段代码,表示建立了一个名为“myCanvas”的canvas网页元素,它就是一块画布,该画布的宽为400,高为300。有了这块画布,我们就可以使用JavaScript编写程序,利用Canvas API在这块画布上绘制图形。如果所用浏览器不支持Canvas API,则就会显示canvas标签中间的文字——“您的浏览器不支持canvas!”。
每个canvas网页元素都有一个对应的context对象(上下文对象),Canvas API定义在这个context对象上面。为了在canvas上绘制图形,必须先得到一个画布上下文对象的引用。为此,使用JavaScript编写程序段如下:
var canvas = document.getElementById('myCanvas'); // 取得网页中的画布对象
var ctx = canvas.getContext('2d'); // 得到画布上下文对象ctx
上面代码中,getContext方法指定参数2d,表示该canvas对象用于生成2D图案(即平面图案)。如果参数是3d,就表示用于生成3D图像(即立体图案)。
当使用一个canvas元素的getContext(“2d”)方法时,返回的是CanvasRenderingContext2D对象,其内部表现为笛卡尔平面坐标。这就是Canvas画布提供的一个用来作图的平面空间,该空间的每个点都有自己的坐标,x表示横坐标,y表示纵坐标。原点(0, 0)位于画布左上角,x轴的正向是原点向右,y轴的正向是原点向下。
每一个canvas元素仅有一个上下文对象。得到了这个上下文对象,就可以利用这个对象的属性和方法进行图形绘制了。
2.绘图方法
2.1 绘制路径
在Canvas API中,上下文CanvasRenderingContext2D对象提供了一系列与图形绘制相关的属性和方法。其中,与路径绘制相关的方法如下:
void beginPath(); // 开始绘制路径
void closePath(); // 结束路径绘制
void moveTo(in float x, in float y); // 设置线段的起点
void lineTo(in float x, in float y); // 设置线段的终点
void bezierCurveTo(in float cp1x, in float cp1y, in float cp2x, in float cp2y, in float x, in float y); // 绘制一条三次贝塞尔曲线
void quadraticCurveTo(in float cpx, in float cpy, in float x, infloat y); // 绘制一条二次贝塞尔曲线
void stroke(); // 给透明的线段着色,从而完成线段绘制
void fill(); // 给闭合路径填充颜色,填充色由fillStyle属性指定
与绘制路径相关的属性有:
attribute float lineWidth; // 线的宽度,默认为1
attribute any strokeStyle; // 着色的颜色,默认为black(黑色)
attribute any fillStyle; // 填充颜色,默认为black(黑色)
attribute DOMString lineCap; // 线段的箭头样式,仅有三个选项:butt(默认值)、round、square,其他值忽略
下面通过几个例子来说明绘制路径的方法和属性的使用。
例1 绘制一条从(20,20)到(200,20)的一条红色横线。
<!DOCTYPE html>
<head>
<title>绘图方法的使用</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
ctx.beginPath(); // 开始路径绘制
ctx.moveTo(20, 20); // 设置路径起点,坐标为(20,20)
ctx.lineTo(200, 20); // 绘制一条到(200,20)的直线
ctx.lineWidth = 1.0; // 设置线宽
ctx.strokeStyle = "#FF0000"; // 设置线条颜色为红色
ctx.stroke(); // 进行线的着色,这时整条线才变得可见
}
</script>
</head>
<body onload="draw('myCanvas');">
<canvas id="myCanvas" width="400" height="300" style="border:3px double #996633;">
</canvas>
</body>
</html>
将上述HTML代码保存到一个html文本文件中,再在浏览器中打开包含这段HTML代码的html文件,可以看到在画布中绘制出一条长为180的红色横线。
下面的例子中我们不再给出完整的HTML文件内容,只给出JavaScript编写的与图形绘制直接相关的代码。例如例1源文件中加了注释的6条语句。读者需要自己试一试时,只需把下列各例给出的代码去覆盖例1中的6条注释语句,其余部分保持不变即可。
在绘制路径时,moveto和lineto方法可以多次使用。最后,还可以使用closePath方法,自动绘制一条当前点到起点的线段,形成一个封闭图形,省却使用一次lineto方法。
例2 在画布中绘制一个红色边框的直角三角形和一个蓝色边框的等腰三角形。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(20,100); // 垂直直角边
ctx.lineTo(70,100); // 水平直角边
ctx.lineTo(20,20); // 斜边
ctx.strokeStyle="red";
ctx.stroke(); // 进行着色,使得线段可见
ctx.beginPath();
ctx.moveTo(40,120);
ctx.lineTo(20,180); // 左边的腰
ctx.lineTo(60,180); // 底边
ctx.closePath(); // 右边的腰是通过自动封闭绘制得到
ctx.strokeStyle="blue";
ctx.stroke();
例3 线段箭头的三种样式的比较。
ctx.lineWidth=10;
ctx.strokeStyle="red";
ctx.beginPath();
ctx.lineCap='butt';
ctx.moveTo(100,50);
ctx.lineTo(250,50);
ctx.stroke();
ctx.beginPath();
ctx.lineCap='round';
ctx.moveTo(100,80);
ctx.lineTo(250,80);
ctx.stroke();
ctx.beginPath();
ctx.lineCap='square';
ctx.moveTo(100,110);
ctx.lineTo(250,110);
ctx.stroke();
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图1所示的三条线段。其中,第1根线段箭头样式为“butt”(为默认值),线段的头和尾都是长方形,也就是不做任何的处理;第2根线段箭头样式为“round”,线段的头和尾都增加一个半圆形的箭头;第3根线段的样式为“square”,线段的头和尾都增加一个长方形,长度为线宽一半,高度为线宽。
图1 绘制的3根红色线段
例4 绘制红绿蓝3个实心三角形。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(20,100);
ctx.lineTo(70,100);
ctx.lineTo(20,20);
ctx.fillStyle="red";
ctx.fill(); // 填充红色三角形
ctx.beginPath();
ctx.moveTo(40,120);
ctx.lineTo(20,180);
ctx.lineTo(60,180);
ctx.closePath();
ctx.fillStyle="green";
ctx.fill(); // 填充绿色三角形
ctx.beginPath();
ctx.moveTo(70,20);
ctx.lineTo(120,180);
ctx.lineTo(140,150);
ctx.fillStyle="blue";
ctx.fill(); // 填充蓝色三角形
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图2所示的三个实心三角形。
图2 三个实心三角形
通过上面的示例,我们可以知道,填充的形状应该是封闭的路径。如果路径未关闭,那么 fill() 方法会从路径结束点到开始点之间添加一条线,以关闭该路径,然后填充该路径。例如,图2中蓝色三角形构成的路径并未关闭,调用fill()时,会自动添加直线关闭。
贝赛尔曲线(Bezier curve)是计算机图形学中相当重要的参数曲线。Canvas API中提供了两个绘制贝塞尔曲线的方法。其中:
quadraticCurveTo() 方法用于绘制一条二次贝塞尔曲线。
二次贝塞尔曲线需要两个点。第一个点(cpx,cpy)是用于二次贝塞尔计算中的控制点,第二个点(x,y)是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在,需要使用 beginPath() 和 moveTo() 方法来定义开始点。
bezierCurveTo() 方法用于绘制一条三次贝塞尔曲线。
三次贝塞尔曲线需要三个点。前两个点(cp1x,cp1y)和(cp2x,cp2y)是用于三次贝塞尔计算中的控制点,第三个点(x,y)是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在,同样需要使用 beginPath() 和 moveTo() 方法来定义开始点。
例5 绘制一条二次贝塞尔曲线和一条三次贝塞尔曲线。
ctx.beginPath();
ctx.moveTo(20,20);
ctx.quadraticCurveTo(20,100,200,20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20,120);
ctx.bezierCurveTo(20,220,200,180,200,120);
ctx.stroke();
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图3所示的两条贝塞尔曲线。
图3 两条贝塞尔曲线
例6 使用多个贝塞尔曲线来绘制一个对话气泡。
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图4所示的对话气泡。
图4 对话气泡
例7 使用多个贝塞尔曲线来绘制一个红心。
ctx.fillStyle="red";
ctx.beginPath();
ctx.moveTo(75,40);
ctx.bezierCurveTo(75,37,70,25,50,25);
ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
ctx.bezierCurveTo(20,80,40,102,75,120);
ctx.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图5所示的红心图案。
图5 红心
例8 通过循环绘制一个9行9列的棋盘。
ctx.strokeStyle="red";
ctx.lineWidth=3;
ctx.beginPath();
for (i=50;i<=450;i+=50)
{
ctx.moveTo(i,50);
ctx.lineTo(i,450);
ctx.moveTo(50,i);
ctx.lineTo(450,i);
}
ctx.stroke();
为显示完整的棋盘,请将画布的宽和高均设置为500。在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图6所示的棋盘。
图6 棋盘
2.2 绘制矩形
在Canvas API中,上下文CanvasRenderingContext2D对象提供的与矩形绘制相关的方法如下:
void rect(in float x, in float y, in float w, in float h); // 建立一个矩形路径
void clearRect(in float x, in float y, in float w, in float h); // 清除给定矩形区域的内容
void fillRect(in float x, in float y, in float w, in float h); // 填充给定的矩形区域
void strokeRect(in float x, in float y, in float w, in float h); // 绘制给定的矩形边框
这个几个方法中给定的四个参数分别为矩形左上角顶点的x坐标、y坐标,以及矩形的宽w和高h。
例9 绘制红绿蓝三个矩形边框。
ctx.beginPath();
ctx.lineWidth="8";
ctx.strokeStyle="red";
ctx.rect(15,15,150,150);
ctx.stroke(); // 绘制红色矩形
ctx.beginPath();
ctx.lineWidth="3";
ctx.strokeStyle="#00FF00";
ctx.strokeRect(30,30,40,50); // 绘制绿色矩形
ctx.beginPath();
ctx.lineWidth="8";
ctx.strokeStyle="blue";
ctx.moveTo(80,50);
ctx.lineTo(80,120);
ctx.lineTo(140,120);
ctx.lineTo(140,50);
ctx.closePath();
ctx.stroke(); // // 绘制蓝色矩形
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图7所示的三个矩形。
图7 三个矩形边框
例10 绘制一个边长为100的正方形,边框采用蓝色,内部用红色填充。
ctx.fillStyle="red";
ctx.strokeStyle="blue";
ctx.lineWidth=2;
ctx.fillRect(50,50,100,100);
ctx.strokeRect(50,50,100,100);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图8所示的正方形。
图8 正方形
在没有进行坐标旋转的情况下,采用rect()和strokeRect()方法绘制的矩形一定是两条边与x轴平行,两条边与y轴平行。若要绘制与坐标轴不平行的矩形,可以采用绘制4条线的方法完成。
例11 绘制矩形中的矩形。要求矩形里的矩形其顶点在外面矩形的中点上。
var x = [50,50,250,250];
var y = [50,250,250,50];
ctx.strokeStyle="red";
ctx.lineWidth=3;
for (i=1;i<=4;i++)
{
ctx.beginPath();
ctx.moveTo(x[0],y[0]);
for (k=1;k<=3;k++)
ctx.lineTo(x[k],y[k]);
ctx.closePath();
ctx.stroke();
var tx=x[0];
var ty=y[0];
for (k=0;k<3;k++)
{
x[k]=(x[k]+x[k+1])/2;
y[k]=(y[k]+y[k+1])/2;
}
x[3]=(tx+x[3])/2;
y[3]=(ty+y[3])/2;
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图9所示的正方形。
图9 正方形中的正方形
例12 绘制国际象棋棋盘。
for (i=0;i<8;i++)
{
for (j=0;j<8;j++)
{
if ((i+j)%2==0)
ctx.fillStyle = 'black';
else
ctx.fillStyle= 'white';
ctx.fillRect(j*50,i*50,50,50);
}
}
为显示完整的棋盘,请将画布的宽和高均设置为400。在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图10所示的国际象棋棋盘。
图10 国际象棋棋盘
例13 根据给定数据绘制柱状图。
var data = [100, 50, 20, 30, 100];
var colors = [ "red","orange", "yellow","green", "blue"];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var i=0; i<data.length; i++)
{
var dp = data[i];
ctx.fillStyle = colors[i];
ctx.fillRect(25+i*50, 280-dp*2, 50, dp*2);
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图11所示的柱状图。
图11 柱状图
2.3 绘制圆形和扇形
在Canvas API中,上下文CanvasRenderingContext2D对象提供的与圆形和弧等绘制相关的方法如下:
void arc(in float x, in float y, in float radius, in float startAngle, in float endAngle, in boolean anticlockwise);
void arcTo(in float x1, in float y1, in float x2, in float y2, in float radius);
其中,arc方法用来绘制扇形。参数x和y是圆心坐标,radius是半径,startAngle和endAngle则是扇形的起始角度和终止角度(以弧度表示),anticlockwise表示作图时应该逆时针画(true)还是顺时针画(false)。
arcTo() 方法用于在画布上创建介于两个切线之间的弧/曲线。绘制出子路径最后一个点(x0,y0)和(x1,y1)以及(x1,y1)和(x2,y2)构成的两条直线间半径为radius的最短弧线,并用直线连接(x0,y0)
例14 绘制圆弧。
ctx.strokeStyle="red";
ctx.fillStyle="orange";
for(var i=0;i<3;i++)
{
for(var j=0;j<4;j++)
{
ctx.beginPath();
x = 50+j*100;
y = 50+i*100;
radius = 45;
startAngle = 0;
endAngle = Math.PI/2+(Math.PI*j)/2;
anticlockwise = i%2==0 ? false : true;
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i>1)
ctx.fill();
else
ctx.stroke();
}
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图12所示的圆弧。
图12 圆弧及填充
通过这个例子还可以加深理解:当调用fill()函数时,所有没有闭合的形状都会自动闭合,因此可以不调用closePath()函数。但是调用stroke()时不会自动闭合。
例15 绘制9个大小不一的圆。
ctx.fillStyle="red";
ctx.lineWidth=1;
for (var i=1;i<10;i++)
{
ctx.beginPath();
ctx.arc(i*20,i*20,i*10,0,Math.PI*2,true);
ctx.closePath();
ctx.fillStyle='rgba(255,0,0,0.25)';
ctx.fill();
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图13所示的图形。
图13 逐渐放大的圆
例16 根据给定数据绘制饼图。
var data = [100, 50, 20, 30, 100];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
var colors = [ "red","orange", "yellow","green", "blue"];
var total = 0;
for(var i=0; i<data.length; i++)
total += data[i];
var prevAngle = 0;
for(var i=0; i<data.length; i++)
{
var fraction = data[i]/total;
var angle = prevAngle + fraction*Math.PI*2;
ctx.fillStyle = colors[i];
ctx.beginPath();
ctx.moveTo(150,150);
ctx.arc(150,150, 100, prevAngle, angle, false);
ctx.lineTo(150,150);
ctx.fill();
ctx.strokeStyle = "black";
ctx.stroke();
prevAngle = angle;
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图14所示的饼图。
图14 饼图
2.4 绘制文本
在Canvas API中,上下文CanvasRenderingContext2D对象提供的与文本绘制相关的方法如下:
void fillText(in DOMString text, in float x, in float y, optionalin float maxWidth);
void strokeText(in DOMString text, in float x, in float y, optionalin float maxWidth);
这两个方法用来绘制文本,它的前三个参数分别为文本内容、起点的x坐标、y坐标。其中:fillText方法为绘制填充的文字;strokeText方法为对文字进行描边,不填充内部区域,通常用来添加空心字。
与文本相关的属性有:
attribute DOMString font; // 设置字体,默认为10px sans-serif
attribute DOMString textAlign; // 设置对齐方式,有"start", "end", "left", "right", "center"等,默认为"start"
attribute DOMString textBaseline; //设置文字对齐基线,有"top", "hanging", "middle", "alphabetic", "ideographic", "bottom" 等取值,默认为”alphabetic"
例17 在画布上添加两行文字。
ctx.font = "Bold 50px 隶书";
ctx.fillStyle = "Black";
ctx.fillText("我们是中国人", 10, 50);
ctx.strokeText("我们热爱我们的祖国", 10, 150);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图15所示的文本。
图15 文本绘制
注意:fillText方法不支持文本断行,即所有文本出现在一行内。所以,如果要生成多行文本,只有调用多次fillText方法。
例18 绘制包含数据说明的柱状图。
var data = [100, 50, 20, 30, 100];
var colors = [ "red","orange", "yellow","green", "blue"];
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var i=0; i<data.length; i++)
{
var dp = data[i];
ctx.fillStyle = colors[i];
ctx.fillRect(25+i*50, 280-dp*2, 50, dp*2);
}
ctx.fillStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(25,10);
ctx.lineTo(25,280);
ctx.lineTo(290,280);
ctx.stroke();
ctx.fillStyle = "black";
for(var i=0; i<6; i++)
{
ctx.fillText((5-i)*20 + "",4, i*40+80);
ctx.beginPath();
ctx.moveTo(25,i*40+80);
ctx.lineTo(30,i*40+80);
ctx.stroke();
}
var labels = ["JAN","FEB","MAR","APR","MAY"];
for(var i=0; i<5; i++)
ctx.fillText(labels[i], 40+ i*50, 290);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图16所示的包含数据说明的柱状图。
图16 包含数据说明的柱状图
还可以为文本等设置阴影。
例19 为文本设置阴影。
ctx.shadowOffsetX = 3; // 设置水平位移
ctx.shadowOffsetY = 3; // 设置垂直位移
ctx.shadowBlur = 2; // 设置模糊度
ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; // 设置阴影颜色
ctx.font = "50px 宋体";
ctx.fillStyle = "Black";
ctx.fillText("我们是中国人", 10, 50);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图17所示的文字阴影效果。
图17 文字阴影
2.5 图形剪切
在Canvas API中,上下文CanvasRenderingContext2D对象提供了一个用于图形剪切的方法。
void clip();
剪切(clip)路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏没有遮罩的部分,如图18所示。红边五角星就是裁切路径,所有在路径以外的部分都不会在 canvas 上绘制出来。默认情况下,canvas 有一个与它自身一样大的剪切路径(也就是没有剪切效果)。
图18 剪切示意图
例20 一个简单的剪切示例。
ctx.fillStyle = 'red';
ctx.fillRect(0,0,400,300);
ctx.beginPath();
ctx.moveTo(200,50);
ctx.lineTo(100,250);
ctx.lineTo(300,250);
ctx.closePath();
ctx.lineWidth = 10;
ctx.stroke();
ctx.clip();
ctx.fillStyle = 'yellow';
ctx.fillRect(0,0,400,150);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图19所示的三角形剪切。
图19 三角形剪切
在这个例子中,先画了一个与 canvas 一样大小(宽400,高300)的红色方形作为背景,然后用 clip方法创建一个三角形的剪切路径。剪切路径创建之后,所有出现在它里面的东西才会画出来。这样在其后绘制高度为画布一半的矩形填充时,只有三角形剪切路径里面的内容才会绘制出来。
设定了剪切区域之后,无论在Canvas上绘制什么,只有落在剪切区域内的那部分才能得以显示,其余都会被遮蔽掉。
例21 cilp方法的进一步理解。
// 绘制第一个圆
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(200, 100, 100, 0, Math.PI * 2, false);
ctx.fill();
// 绘制第二个圆
ctx.beginPath();
ctx.fillStyle = 'blue';
ctx.arc(100, 150, 100, 0, Math.PI * 2, false);
ctx.fill();
// 绘制第三个圆
ctx.beginPath();
ctx.fillStyle = 'green';
ctx.arc(300, 150, 100, 0, Math.PI * 2, false);
ctx.fill();
// 绘制第四个圆
ctx.beginPath();
ctx.fillStyle = 'brown';
ctx.arc(200, 200,100, 0, Math.PI * 2, false);
ctx.fill();
ctx.lineWidth = 10;
ctx.strokeStyle='black';
ctx.stroke();
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图20所示的4个圆。
图20 没有使用clip方法的4个圆
若在第3个圆绘制后插入一条语句“ctx.clip();”,则在画布中绘制出如图21所示的图形。从图21可以看出第4个圆(棕色的),只有落在第3个圆中的部分被绘制出来。
图21 在第3个圆之后使用clip()方法
若clip()方法往上移,放到第2个圆的后面,则在画布中绘制出如图22所示的图形。从图22可以看出第3个圆(蓝色的)完全没有被绘制出来,因为第3个圆与第2个圆相切,没有交集;第4个圆(棕色的)只有落在第2个圆中的部分被绘制出来。
图22 在第2个圆之后使用clip()方法
若再把clip()方法往上移,放到第1个圆的后面,则在画布中绘制出如图23所示的图形。从图23可以看出,第1个圆为剪切区域,第2、3、4个圆只有落在第1个圆中的部分才被绘制出来。
图23 在第1个圆之后使用clip()方法
当使用剪切函数clip()进行绘图后,可能需要取消该剪切区域或者重新定义剪切区域。在Canvas中,可以通过save()函数和restore()函数来实现。在构建剪切区域之前保存状态,完成剪切区域内的绘图之后进行状态读取。
例如,在例21的程序中,在第2个圆绘制后插入语句“ctx.save();”和“ctx.clip();”,在第3个圆绘制后插入语句“ctx.restore();”,则在画布中绘制出如图24所示的图形。从图24可以看出,第3个圆(蓝色的)完全没有被绘制出来,因为第3个圆与第2个圆相切,没有交集;第4个圆(棕色的)全部被绘制出来,此时取消了剪切区域。
图24 在第3个圆绘制后取消剪切区域
例22 采用clip实现简单的探照灯效果。
<!DOCTYPE html>
<head>
<title>简单探照灯</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var rot=10;
var canvas=document.getElementById('myCanvas');
var ctx=canvas.getContext('2d');
setInterval("draw()",100);
function draw()
{
ctx.clearRect(0,0,400,400);
ctx.save();
ctx.fillStyle="black";
ctx.fillRect(0,0,400,400);
ctx.beginPath();
ctx.arc(rot,200,40,0,Math.PI*2,true);
ctx.closePath();
ctx.fillStyle="white";
ctx.fill();
ctx.clip();
ctx.font="bold 45px 隶书";
ctx.textAlign="center";
ctx.textBaseline="middle";
ctx.fillStyle="#FF0000";
ctx.fillText("中国北京欢迎您!",200,200);
ctx.restore();
rot=rot+10;
if (rot>400) rot=10;
}
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以看到在画布中呈现出如图25所示的简单探照灯效果。
图25 简单的探照灯
3.图形变换
在图形学中,可以对图形进行平移、缩放和旋转等变换操作。
在Canvas API中,上下文CanvasRenderingContext2D对象提供的与图形变换相关的方法如下:
void translate(in float x, in float y); // 平移Canvas的原点到指定的坐标点(x,y)
void rotate(in float angle); // 按给定的弧度angle顺时针旋转
void scale(in float x, in float y); // 按给定的缩放倍率进行缩放
void setTransform(in float m11, in float m12, in float m21, infloat m22, in float dx, in float dy); // 将当前转换重置为单位矩阵
void transform(in float m11, in float m12, in float m21, in floatm22, in float dx, in float dy); // 按矩阵进行变换
在进行图形变换前先保存上下文环境(状态)是一个良好的习惯。大多数情况下,调用 restore()方法比手动恢复原先的状态要简单得多。例如,在一个循环中做平移操作但没有保存和恢复canvas 的状态,很可能到最后会发现有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
3.1 save和restore方法
save方法用于保存上下文环境,restore方法用于恢复到上一次保存的上下文环境。
在Canvas中,每个上下文对象都包含一个绘图状态的堆,绘图状态包含下列内容:
(1)当前的变换矩阵;
(2)当前的剪切区域(clip);
(3)当前的属性值:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、 lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、 strokeStyle、textAlign、textBaseline等。
例23 save方法和restore方法的简单应用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.save();
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 5;
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.fillStyle = "blue";
ctx.fillRect(100,10,80,80);
ctx.restore();
ctx.fillRect(200,10,80,80);
ctx.fillStyle = "orange";
ctx.fillRect(300,10,80,80);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图26所示的4个矩形。
图26 4个矩形
这4个矩形中,第1个矩形填充红色,之后保存状态(填充色为红色),第2个矩形是一个有黑色阴影的填充色为蓝色的矩形;接着,使用restore方法,恢复了保存前的设置,绘制了一个没有阴影的填充色为红色的第3个矩形,第4个矩形是一个填充色为橙色的矩形,也没有阴影。
若去掉代码中的“ctx.restore();”语句,不恢复状态,则绘制的4个矩形如图27所示。体会图27与图26的区别。
图27 不执行“ctx.restore();”绘制的4个矩形
3.2 translate、scale和rotate方法
translate() 方法实现坐标平移,例如,进行ctx.translate(dx,dy);后,坐标原点移到(dx,dy)处,这样程序绘图时给出的坐标值(x,y),相对于canvas默认的坐标原点(0,0),应该为(x+dx,y+dy)。
例24 translate简单应用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.fillStyle = "blue";
ctx.translate(80,80);
ctx.fillRect(10,10,80,80);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图26所示的2个矩形。从图中看出,在坐标位置 (10,10) 处绘制一个红色填充矩形后,将坐标原点平移到(80,80),这样再次绘制填充蓝色的矩形从位置 (90,90) 处开始绘制。
图28 平移后的蓝色矩形
例25 连续坐标平移的示例。
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<5; i++)
{
ctx.translate(50,50);
ctx.fillRect(0,0,100,100);
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图29所示的5个矩形。
图29 坐标平移后的5个矩形
scale() 方法实现图形的缩放。需要注意的是使用scale方法对绘图进行缩放后,所有之后的绘图也会被缩放,包括坐标定位也会被缩放。例如,执行ctx.scale(2,2)后,绘图将定位于距离画布左上角两倍远的位置。
例26 scale简单应用示例。
ctx.fillStyle = "red";
ctx.fillRect(10,10,80,80);
ctx.save();
ctx.fillStyle = "blue";
ctx.scale(2,2);
ctx.fillRect(50,10,80,80);
ctx.restore();
ctx.fillStyle = "green";
ctx.scale(0.5,0.5);
ctx.fillRect(10,200,80,80);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图30所示的3个矩形。从图中看出,在坐标位置 (10,10) 处绘制一个红色填充矩形后,ctx.scale(2,2);将绘图放大两倍,这样绘制填充蓝色的矩形从位置 (100,20) 处开始绘制,其宽和高均是红色填充矩形的2倍;恢复上下文环境后,ctx.scale(0.5,0.5);将绘图缩小1倍,这样绘制填充绿色的矩形从位置 (5,100) 处开始绘制,其宽和高均是红色填充矩形的一半。
图30 矩形的缩放
若去掉代码中的“ctx.restore();”语句,不恢复状态,则绘制的3个矩形如图31所示。体会图31与图30的区别。绿色矩形的大小之所以与红色矩形一样,是因为一个东西放大2倍后再缩小1倍,正好恢复原样。
通过这个示例一定得明白,图形变换的设置一定是在前一个状态的基础上进行的。因此在进行图形变换时,根据需要通过save()方法保存状态,restore()方法恢复状态是非常重要的。
图31 不执行“ctx.restore();”绘制的3个矩形
例27 连续图形放大的示例。
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<4; i++)
{
ctx.scale(2,2);
ctx.fillRect(5,5,10,10);
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图32所示的4个矩形。
图32 依次放大后的4个矩形
rotate()方法实现图形的旋转,其中参数给出的旋转角度angle以弧度计。如需将角度degress转换为弧度,可以使用 degrees*Math.PI/180 公式进行计算。
例28 将矩形旋转45°。
ctx.fillStyle = "red";
ctx.fillRect(150,50,80,80);
ctx.fillStyle = "blue";
ctx.rotate(45*Math.PI/180);
ctx.fillRect(150,50,80,80);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图33所示的2个矩形,其中蓝色矩形顺时针旋转了45°。
图33 顺时针旋转了45°的蓝色矩形
例29 连续旋转的矩形。
ctx.translate(200,200);
ctx.fillStyle = 'rgba(255,0,0,0.5)';
for (i = 0; i<6; i++)
{
ctx.rotate(Math.PI/3);
ctx.fillRect(0,0,100,50);
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图34所示的6个矩形。
图34 旋转的矩形
3.3 transform 和setTransform方法
transform() 方法是通过变换矩阵实现变换。例如,语句ctx.transform(a,b,c,d,e,f);中包含6个参数,其中:
a 水平缩放绘图
b 水平倾斜绘图
c 垂直倾斜绘图
d 垂直缩放绘图
e 水平移动绘图
f 垂直移动绘图
通过transform() 方法可以缩放、旋转、移动并倾斜当前的绘图环境。
例如,ctx.translate(dx,dy)可以用
context.transform(0,1,1,0,dx,dy);
或 context.transform(1,0,0,1,dx,dy); 来替代。
又例如, ctx.transform(0.95,0,0,0.95,30,30);
可以替代 ctx.translate(30,30);
ctx.scale(0.95.0.95); 同时缩放和平移。
例30 transform() 方法简单应用示例。
ctx.fillStyle="red";
ctx.fillRect(0,0,150,50)
ctx.transform(1,0.5,0,1,30,10);
ctx.fillStyle="blue";
ctx.fillRect(0,0,150,50);
ctx.transform(1,0.5,-0.5,1,30,10);
ctx.fillStyle="green";
ctx.fillRect(0,0,150,50);
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图35所示的3个矩形。由图可以看出,蓝色矩形水平方向倾斜了,绿色矩形在水平方向和垂直方向上都倾斜了。
图35 应用transform() 方法后绘制的矩形
setTransform()方法也是通过变换矩阵实现变换,它与transform()的使用方法类同,区别在于:每次调用 setTransform() 时,都会重置前一个变换矩阵然后构建新的矩阵;二每次调用 transform() 时,它都会在前一个变换矩阵基础上构建变换矩阵。
例如,将例30中的第2个transform 方法“ctx.transform(1,0.5,-0.5,1,30,10);”改写为“ctx.setTransform(1,0.5,-0.5,1,30,10);”,则在浏览器窗口中绘制出如图36所示的图形。由图可知,蓝色矩形基本被绿色矩形覆盖,只露出一个小角。这是因为绘制蓝色矩形之前,使用的是setTransform() 方法,会重置变换矩阵,而不是在绘制蓝色矩形的变换矩阵基础上进行,因此蓝色矩形与绿色矩形水平倾斜一致,水平与垂直方向平移也一致,只是绿色矩形多进行了垂直倾斜,因此将蓝色矩形露出一小角。
图36 应用setTransform() 方法后绘制的矩形
例31 通过变换绘制螺旋。
ctx.translate(200,50);
for(var i=0; i<50; i++)
{
ctx.save();
ctx.transform(0.95,0,0,0.95,30,30);
ctx.rotate(Math.PI/12);
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,0.5)';
ctx.arc(0,0,50,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
}
在浏览器中打开包含这段JavaScript代码的html文件,可以看到在画布中绘制出如图37所示的螺旋图形。
图37 螺旋图
4.图像处理
canvas更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面等等。浏览器支持的任意格式的外部图片都可以使用,比如PNG、GIF或者JPEG。
4.1 图像绘制
drawImage() 方法可以向画布上绘制图像、画布或视频。
要在画布上绘制图片,可以使用drawImage方法。该方法有三种不同的调用形式:
(1)ctx.drawImage(img,x,y); // 在画布上定位图像
(2)ctx.drawImage(img,x,y,width,height);
// 在画布上定位图像,并规定图像的宽度和高度
(3)ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
// 剪切图像,并在画布上定位被剪切的部分。
其中,各参数的说明如下:
img 规定要使用的图像、画布或视频。
sx 可选。开始剪切的 x 坐标位置。
sy 可选。开始剪切的 y 坐标位置。
swidth 可选。被剪切图像的宽度。
sheight 可选。被剪切图像的高度。
x 在画布上放置图像的 x 坐标位置。
y 在画布上放置图像的 y 坐标位置。
width 可选。要使用的图像的宽度。(伸展或缩小图像)
height 可选。要使用的图像的高度。(伸展或缩小图像)
在使用drawImage()方法将图像绘制到画布上之前,要获得需要绘制的图像。canvas的API可以使用图像源的类型有:
(1)HTMLImageElement。
这些图片或者由Image()函数构造出来的,或者页面中的某一个<img>元素。
(2)HTMLVideoElement。
用一个HTML的 <video>元素作为图片源,可以从视频中抓取当前帧作为一个图像。
(3)HTMLCanvasElement。
可以使用另一个 <canvas> 元素作为图片源。这种类型的一个常用的应用就是将第二个canvas作为另一个大的 canvas 的缩略图。
获取到需要在canvas上绘制的图片的方式有以下几种:
(1)使用相同页面内的图片或其它 canvas 元素。
通过document.images 集合、document.getElementsByTagName 方法或者 document.getElementById 方法来获取页面内的图片(如果已知图片元素的 ID)。
使用其它 canvas 元素和引用页面内的图片类似地,用 document.getElementsByTagName 或 document.getElementById 方法来获取其它 canvas 元素。
例32 在画布中绘制页面上的图片。
<!DOCTYPE html>
<head>
<title>在画布中绘制页面上的图片</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
var img=document.getElementById("myImg");
ctx.drawImage(img,10,10);
}
</script>
</head>
<body onload="draw('myCanvas');">
<img src="aaa.jpg" id="myImg">
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
</body>
</html>
在浏览器中打开保存这段HTML代码的html文件,可以看到在画布中出现如图38所示的页面。页面中,左边的图片是页面中的img元素,右边是在画布中绘制的左边的图片。
图38 图片绘制(一)
若将上面程序中的“ctx.drawImage(img,10,10);”改写为“ctx.drawImage(img,50,50,200,200,20,20,250,150);”,则在浏览器中显示如图39所示的内容。右边canvas中绘制的图片是左边原图的部分剪切,并进行了水平拉伸。
图39 图片绘制(二)
(2)创建图像。
用脚本创建一个新的 HTMLImageElement 对象。要实现这个方法,可以使用Image()构造函数。
var img = new Image(); // 创建一个img元素
img.src = 'myImage.png'; // 设置图片源地址
当脚本执行后,图片开始装载。
由于图像的载入需要时间,drawImage方法只能在图像完全载入后才能调用,因此上面的代码需要改写。可用load时间来保证不会在加载完毕之前使用这个图片。
var img = new Image(); // 创建img元素
img.onload = function(){
// 执行drawImage语句
}
img.src = 'myImage.png'; // 设置图片源地址
例33 编写如下的HTML文件内容。
<!DOCTYPE html>
<head>
<title>创建图像对象使用图片进行绘制</title>
<script type="text/javascript">
function draw(id)
{
var canvas=document.getElementById(id);
if (canvas==null)
return false;
var ctx=canvas.getContext('2d');
var image = new Image();
image.src = 'aaa.jpg';
image.onload=function(){
ctx.drawImage(image,10,10);
}
}
</script>
</head>
<body onload="draw('myCanvas');">
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
</body>
</html>
在浏览器中打开保存这段HTML代码的html文件,可以看到在画布中出现如图40所示的页面。
图40 创建图像对象使用图片进行绘制
(3)通过 data: url 方式嵌入图像。
Data urls 允许用一串 Base64 编码的字符串的方式来定义一个图片。其优点就是图片内容即时可用,无须再到服务器兜一圈。缺点就是图像没法缓存,图片大的话内嵌的 url 数据会相当的长。
4.2 像素级操作
在Canvas API中,上下文CanvasRenderingContext2D对象提供了三个方法用于像素级操作:createImageData, getImageData, 和putImageData。
(1)createImageData()方法。
createImageData() 方法创建新的空白ImageData 对象。新对象的默认像素值 transparent black。
ImageData对象保存了图像像素值。每个对象有三个属性:width,height和data。data 属性类型为CanvasPixelArray,用于储存width*height*4个像素值。每一个像素有RGB值和透明度alpha值(其值为 0 至255,包括alpha在内)。像素的顺序从左至右,从上到下,按行存储。其中:
R - 红色 (0-255)
G - 绿色 (0-255)
B - 蓝色 (0-255)
A - alpha 通道 (0-255;0 是透明的,255 是完全可见的)
transparent black 表示 (0,0,0,0)。
像素值以数组形式存在,由于每个像素有4条信息,因此数组的大小是 ImageData 对象的4倍。可以使用 ImageDataObject.data.length获得数组的大小。
createImageData() 方法有两种调用形式:
1) var imgData=ctx.createImageData(width,height);
以指定的尺寸(以像素计)创建新的 ImageData 对象,其中参数width给定ImageData 对象的宽度,height给定ImageData 对象的高度。
2)var imgData=ctx.createImageData(imageData);
创建与指定的另一个 ImageData 对象尺寸相同的新 ImageData 对象(不会复制图像数据)。
(2)putImageData()方法。
putImageData() 方法用于将图像数据(从指定的 ImageData 对象)放回画布上。其调用格式为:
ctx.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
各参数的说明如下:
imgData 规定要放回画布的 ImageData 对象。
x ImageData 对象左上角的 x 坐标,以像素计。
y ImageData 对象左上角的 y 坐标,以像素计。
dirtyX 可选。水平值(x),以像素计,在画布上放置图像的位置。
dirtyY 可选。垂直值(y),以像素计,在画布上放置图像的位置。
dirtyWidth 可选。在画布上绘制图像所使用的宽度。
dirtyHeight 可选。在画布上绘制图像所使用的高度。
例34 createImageData和putImageData方法的简单应用。
var imgData=ctx.createImageData(100,100);
for (var i=0;i<imgData.data.length;i+=4)
{
imgData.data[i+0]=255;
imgData.data[i+1]=0;
imgData.data[i+2]=0;
imgData.data[i+3]=255;
}
ctx.putImageData(imgData,10,10);
上述代码执行后,会在画布中绘制一个红色矩形,矩形的左上角坐标为(10,10),宽度和高度均为100。
(3)getImageData()方法。
getImageData方法可以用来读取(复制)Canvas的内容,返回一个ImageData对象,包含了每个像素的信息。其调用形式为:
var imgData=ctx.getImageData(x,y,width,height);
各参数描述如下:
x 开始复制的左上角位置的 x 坐标。
y 开始复制的左上角位置的 y 坐标。
width 将要复制的矩形区域的宽度。
height 将要复制的矩形区域的高度。
imgData对象有一个data属性,它的值是一个一维数组。该数组的值依次是每个像素的红、绿、蓝、alpha通道值,每个值的范围是0~255。可以通过操作这个数组的值达到操作图像的目的。修改这个数组以后,使用putImageData方法将数组内容重新绘制在Canvas上。
例35 通过操作像素数组的值来操作图像。
<!DOCTYPE html>
<head>
<title>通过操作像素数组的值来操作图像</title>
<script type="text/javascript">
function draw(id1,id2)
{
var canvas1=document.getElementById(id1);
var ctx1=canvas1.getContext('2d');
var canvas2=document.getElementById(id2);
var ctx2=canvas2.getContext('2d');
var imgData1=ctx1.createImageData(100,100);
for (i=0;i<imgData1.data.length;i+=4)
{
imgData1.data[i+0]=255;
imgData1.data[i+1]=0;
imgData1.data[i+2]=0;
imgData1.data[i+3]=255;
}
ctx1.putImageData(imgData1,10,10);
var imgData2=ctx1.getImageData(10,10,100,100);
for (i=imgData2.data.length/4;i<imgData2.data.length*3/4;i+=4)
{
imgData2.data[i+0]=0;
imgData2.data[i+2]=255;
}
ctx2.putImageData(imgData2,10,10);
}
</script>
</head>
<body onload="draw('myCanvas1','myCanvas2');">
<canvas id="myCanvas1" width="200" height="200" style="border:3px double #996633;">
</canvas>
<canvas id="myCanvas2" width="200" height="200" style="border:3px double #996633;">
</canvas>
</body>
</html>
在浏览器中打开保存这段HTML代码的html文件,可以看到在浏览器窗口中显示如图41所示的内容。
图41 通过操作像素数组的值来操作图像的示例
关于更多的Canvas API的知识,可以参阅文档“HTML 5 Canvas 参考手册”(https://www.w3school.com.cn/tags/html_ref_canvas.asp)。