Canvas
简介
canvas是 html5 提供的一个新的接口。常用于绘制图表,绘制图片,制作动画等工作。现代浏览器基本都对canvas兼容了
图表
例如我们在以后的开发工作中,经常会用到 echarts工具,它的就是基于canvas进行绘制的
动画
基本用法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 用于绘制 图形的 canvas元素 -->
<canvas id="myCanvas" width="200" height="200"></canvas>
</body>
</html>
<script>
// 获取 canvas对象
var canvas = document.getElementById('myCanvas');
// 判断当前浏览器是否支持canvas对象
if (canvas.getContext) {
// 获取2D画布
var ctx = canvas.getContext('2d');
// 设置填充颜色
ctx.fillStyle = "rgb(200,200,0)";
// 填充矩形
ctx.fillRect (10, 10, 55, 50);
}
</script>
画布栅格
canvas元素并非Canvas中最强大的部分,真正的关键部分是2D渲染上下文,这是你真正绘制图形的地方。canvas元素的用途只是作为2D渲染上下文的包装器,它包含绘图和图形操作所需要的全部方法和丰富功能。理解这一点是很重要的,强调一下:绘图是在2D渲染上下文中进行的,而不是在canvas元素中进行。可以通过canvas元素访问和显示2D渲染上下文。
坐标系统
2D渲染上下文是一种基于屏幕的标准绘图平台。与其他的2D平台类似,它采用平面的笛卡儿坐标系统,左上角为原点(0,0)。向右移动时,x坐标值会增加;向下移动时,y坐标值会增加。如果你想把图形绘制到正确的位置上,一定要理解这个坐标系统
坐标系统的1个单位通常相当于屏幕的1个像素,所以位置(24,30)是向右24像素和向下30像素的位置。有时候坐标系统的1个单位相当于2个像素(例如,在一些高分辨率显示器中),但是一般的经验法则是1个坐标单位等于1个屏幕像素。
访问2D渲染上下文
我们先创建只有一个空白canvas元素的简单HTML网页:
<canvas id="myCanvas" width="200" height="200">
<!-- Insert fallback content here -->
</canvas>
在这个例子中,我们将这个canvas元素赋值给一个变量,然后再通过调用getContext方法将得到的2D渲染上下文赋给另一个变量。 必须强调一点,由于我们使用了jQuery,所以需要调用get方法才能访问DOM中的canvas元素,然后才能够访问Canvas的getContext方法。
有了包含2D渲染上下文的变量之后,就可以开始绘制图形了。在上下文变量声明语句之后添加下面这行代码:
context.fillRect(0,0,200,200);
完整代码如下
// 获取 canvas对象
var canvas = document.getElementById('myCanvas');
// 判断当前浏览器是否支持canvas对象
if (canvas.getContext) {
// 获取2D画布
var ctx = canvas.getContext('2d');
// 填充矩形
context.fillRect(0,0,200,200);
}
上述代码将在画布上绘制如下的一个黑色矩形,从坐标(0,0)处开始,长宽均为200px:
这个矩形是黑色的,因为Canvas所绘制元素的默认颜色是黑色。
绘制形状和线条
绘制一个正方形是非常简单的。只需要使用一行代码,即调用fillRect方法,语法如下:
context.fillRect(x, y, width, height);
context.fillRect(40, 40, 100, 100);
创建一个矩形需要输入4个参数.前两个参数是正方形原点(左上角)的(x,y)坐标值,其余两个参数是矩形的宽度和高度。矩形宽度是(x,y)位置向右绘制的距离,而矩形高度是(x,y)位置向下绘制的距离。
例如下列代码在(10,20)位置绘制一个宽为200高为100的矩形:
context.fillRect(10,20,200,100);
与fillRect相对应的方法是strokeRect。fillRect绘制一个矩形并给它填充颜色,strokeRect则绘制一个矩形并给它绘制边框,也就是用线条绘制出矩形的轮廓。矩形现在加上了轮廓线,它实际上变成了中空的。
线条绘制
线条与绘制图形有一些区别。它们实际上称为路径。要绘制一个简单的路径,首先必须在2D渲染上下文中调用beginPath方法,意思实际上就是说:“准备,要开始画路径了。” 下一个调用的方法是moveTo,它会设置要绘制路径的原点坐标(x,y)。然后调用lineTo方法设置线条的终点坐标(x,y),再调用closePath完成路径的绘制。最后,调用stroke绘制它的轮廓,显示线条。将全部步骤放到一起,就得到了下面的代码:
context.beginPath(); // Start the path
context.moveTo(40, 40); // Set the path origin
context.lineTo(340, 40);// Set the path destination
context.closePath(); // Close the path
context.stroke(); // Outline the path
最后,我们得到一条直线
但是,直线并不一定是水平或垂直的,通过修改lineTo方法的坐标(x,y)参数,就能够绘制出斜线:
context.lineTo(340,340);
直线本身并没有什么特别的,但是通过组合,它们能够产生复杂且令人惊奇的图形。
圆形
圆形是一个非常复杂的形状,因此Canvas实际上并没有专门绘制圆形的方法。但是有一个方法可以绘制圆弧,圆弧实际上是圆形的组成部分——首尾相连的圆弧就是圆形。在Canvas中创建一个圆形的代码如下:
context.beginPath(); // Start the path
context.arc(230, 90, 50, 0, Math.PI*2, false); // Draw a circle
context.closePath(); // Close the path
context.fill(); // Fill the path
创建一个圆弧需要使用6个参数:圆弧原点的(x,y)坐标值(也是我们例子中的圆心)、圆弧半径、开始角度、结束角度和一个布尔值,如果圆弧按逆时针方向绘制,那么它为true,否则它为false。方法arc可以重写为下面更具可读性的形式:
context.arc(x, y, radius, startAngle, endAngle, anticlockwise);
前面三个参数都很简单。开始角度和结束角度参数表面上很简单,但是需要适当解释才能够很好地理解它们的使用方法。简言之,在Canvas中,一条弧线是由一条曲线定义的,它从与原点(x,y)距离为一个半径且角度为开始角度的位置开始。这条路径最后停在离原点仅(x,y)一个半径且角度为结束角度的位置上
一定要注意,Canvas中的角度是以弧度而不是角度为单位的。简单地说,360度(一个完整的圆)是2π(pi的2倍)弧度。将角度换算成弧度可以按照以下公式进行换算(JavaScript语句):
var degrees = 1; // 1 degree
var radians = degrees * (Math.PI / 180); // 0.0175 radians
如果采用弧度就不需要进行角度到弧度的换算了。
在这个例子中,我们所画弧线的开始角度是0,结束角度是Math.PI*2(pi乘以2),它们就是圆的开始和结束角度。
要在JavaScript中使用pi的值,你需要使用Math时象,它是一个特殊对象,允许你完成各种强大的数学计算。我们还会在其他一些任务中使用这个对象,如生成随机数。
运行这个例子,会在浏览器上看到如图所示的显示结果。
那么,如果想要画一个半圆,应该如何设置结束角度呢?非常简单,其JavaScript代码如下:
context.arc(230, 90, 50, 0, Math.PI, false); // Draw a semi-circle
如果一切正常,应该会在浏览器上看到一个半圆
你还可以对角度进行任意调整,以创建1/4圈和任意饼形。
修改形状和路径的填充颜色
通过设置2D渲染上下文的fillStyle属性,你就能够修改形状和路径的填充颜色。代码如下:
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(40, 40, 100, 100);
在这个例子中,我们赋值了一个“rgb(红、绿、蓝)”颜色值,但是你也可以使用任何有效的css颜色值,如十六进制码(例如,#FF0000)或单词“red”。在这个例子中,颜色值设置为红色(纯红色,没有绿色和蓝色)。
在设置fillStyle属性之后,你所绘制的所有图形都会采用这个颜色。如果你接受这个结果,它就不是问题.但是如果你只希望修改一个对象的颜色,那么你一定要注意。有一个方法可以解决这个问题,就是当你在Canvas上绘制对象时,将fillStyle属性设置回黑色(或其他颜色),例如:
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(40, 40, 100, 100); // Red square
context.fillRect(180, 40, 100, 100); // Red square
context.fillStyle = "rgb(0, 0, 0)";
context.fillRect(320, 40, 100, 100); // Black square
还可以在描边图形和路径上使用strokeStyle属性实现变色效果。例如,下面的代码与前一个例子相同,唯一区别是它使用笔画描边而不是填充:
context.strokeStyle = "rgb(255, 0, 0)";
context.strokeRect(40, 40, 100, 100); // Red square
context.strokeRect(180, 40, 100, 100); // Red square
context.strokeStyle = "rgb(0, 0, 0)";
context.strokeRect(320, 40, 100, 100); // Black square
这其中并没有什么复杂的地方,所有代码都非常简单。修改线条的颜色也是非常简单的:
context.strokeStyle = "rgb(255, 0, 0)"; context.beginPath();
context.moveTo(40, 180);
context.lineTo(420, 180); // Red line
context.closePath();
context.stroke();
context.strokeStyle = "rgb(0, 0, 0)";
context.beginPath();
context.moveTo(40, 220);
context.lineTo(420, 220); // Black line
context.closePath();
context.stroke();
修改线宽
修改颜色很有意思,但是我们例子中的线条还有些细。Canvas有一个方法可以增加线宽,即2D渲染上下文的lineWidth属性。lineWidth属性的默认值为1.但是可以将它修改为任意值。例如,修改红线和黑线的宽度:
context.lineWidth = 5; // Make lines thick
context.strokeStyle = "rgb(255, 0, 0)";
context.beginPath();
context.moveTo(40, 180);
context.lineTo(420, 180); // Red line
context.closePath();
context.stroke();
context.lineWidth = 20; // Make lines even thicker
context.strokeStyle = "rgb(0, 0, 0)";
context.beginPath();
context.moveTo(40, 220);
context.lineTo(420, 220); // Black line
context.closePath();
context.stroke();
其结果是得到一条稍粗的红线和一条非常粗的黑线
lineWidth属性也会影响图形:
context.lineWidth = 5; // Make lines thick
context.strokeStyle = "rgb(255, 0, 0)";
context.strokeRect(40, 40, 100, 100); // Red square
context.strokeRect(180, 40, 100, 100); // Red square
context.lineWidth = 20; // Make lines even thicker
context.strokeStyle = "rgb(0, 0, 0)";
context.strokeRect(320, 40, 100, 100); // Black square
最终得到两个边框稍粗的红色正方形和一个边框非常粗的黑色正方形
绘制文字
Canvas中的文本是以图像形式绘制的,这意味着它无法像HTML文档中的普通文字一样用鼠标指针选取——它实际上不是文本,只是像文本而已。一旦文字绘制之后,它就无法编辑,除非先擦除文字,再重新绘制。在Canvas中绘制文本的好处是你可以利用Canvas支持的强大转换和其他绘图功能。然而,除非你有充分理由不使用普通的HTML元素,否则一定不要在Canvas中创建文本。相反,你应该使用普通的HTML元素来创建文本,然后使用css定位到Canvas之上。关键是使用HTML来处理文本(内容),而使用Canvas来处理像素和图形。
var text = "Hello, World!";
context.fillText(text, 40, 40);
这就是绘制文本所需要的代码。2D渲染上下文的fillText方法可按受4个参数(其中一个是可选的,所以我们现在省略了)。第一个参数是准备绘制的文本,第二个和第三个参数是文本原点(左下角)的(x,y)坐标值。
由于字号太小无法看清楚,这是因为Canvas的默认文本设置是10px sans-serif(非常小)。所以现在修改字号,同时也会介绍修改字体的方法。要实现这个操作,你需要设置2D渲染上下文的font属性,如下所示:
var text = "Hello, World!";
context.font = "30px serif"; // Change the size and font
context.fillText(text, 40, 40);
属性font可接受与css的font属性完全相同的字符串值。在前一个例子中,我们指定了字体的像素大小,然后是希望使用的字体。设置为serif表示计算机的默认字体是serif字体(与Times New Roman类似)。所有代码组合在一起将得到如图1所示的结果。
这样显示效果会好—些,能看清了。如果愿意,甚至可以将文本设置为斜体:
var text = "Hello, World!";
context.font = "italic 30px serif";
context.fillText(text, 40, 40);
这里所做的唯—修改就是将单词italic添加到font属性中,这样就得到如图2所示的结果。
描边文本
var text = "Hello, World!";
context.font = "italic 60px serif";
context.strokeText(text, 40, 100);
这次使用的是strokeText方法,它的参数与fillText完全相同。字号过小会让文本难以辨别,所以在这个例子中,我们加大了字号,而原点也稍微向下移,所以文本不会超出屏幕顶部。最终得到的结果如图3所示。
擦除
clearRect方法
假设你在Canvas上只画了一个正方形和圆形:
context.fillRect(40, 40, 100, 100);
context.beginPath();
context.arc(230, 90, 50, 0, Math.PI*2, false);
context.closePath();
context.fill();
然后,可以随时清除Canvas。要执行这个操作,只需要使用Canvas的原点坐标(x.y)、宽度和高度调用clearRect。如果Canvas宽500像素高500像素,那么可以按照以下方式调用clearRect:
context.clearRect(0, 0, 500, 500);
当运行时,它在浏览器上不会显示任何内容,因为刚刚清除了Canvas的所有内容。甚至,即使不知道Canvas尺寸,也可以使用jQuery的width和height方法来调用clearRect,方法如下:
context.clearRect(0, 0, canvas.width(), canvas.height());
完整的例子如下所示:
var canvas = $("#myCanvas");
var context = canvas.get(0).getContext("2d");
context.fillRect(40, 40, 100, 100);
context.beginPath();
context.arc(230, 90, 50, 0, Math.PI*2, false);
context.closePath();
context.fill();
context.clearRect(0, 0, canvas.width(), canvas.height());
我在这个例子中加入了原始的canvas变量,目的是为了提醒你,我们调用的是哪个对象的clearRect方法。
但是,并不一定要清除整个Canvas,可以只清除Canvas的一个特定区域。例如,如果我们只想清除例子中的正方形,可以按以下方式调用clearRect:
context.clearRect(40, 40, 100, 100);
这样就剩下一个圆形。
这种方法是通过修改clearRect的参数来清除一个特定区域。在我们的例子中,我们将准备擦除的区域的原点(左上角)移动到正方形的左上角(40,40),并将准备擦除的区域的宽度和高度设置为正方形的宽度和高度(100)。其结果是只将正方形所在的特定区域清除。
按照以下方式修改clearRect的参数,就能够将圆形清除:
context.clearRect(180, 40, 100, 100);
如果计算正确,画布中将只剩下正方形。
记住,弧形的原点是它的中心,所以为了获得clearRect方法所需要的正确原点,我们需要用原点的x和y坐标减去它的半径。 Canvas中的对象是可以被部分擦除的,虽然你可能并不需要这样做:
context.fillRect(40, 40, 100, 100);
context.beginPath();
context.arc(230, 90, 50, 0, Math.PI*2, false);
context.closePath();
context.fill();
context.clearRect(230, 90, 50, 50);
这个例子会切掉圆形的一部分
我们可以先绘制一个基本形状,然后再去掉一部分,从而快速方便地绘制出一些复杂的形状。
重新设置画布宽高
如果只是想要擦除Canvas上的所有内容,并从零开始绘图,那么你可以考虑使用重新设置画布宽高。 其依据是每当重新设置一个canvas元素的width和height属性时,Canvas都会自动清除内容并返回其原始状态。 这种方法也有一些缺点,会清除之前所有设置的样式而不仅仅是内容,我们先看一个例子:
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(40, 40, 100, 100);
context.beginPath();
context.arc(230, 90, 50, 0, Math.PI*2, false);
context.closePath();
context.fill();
上面的代码仅仅是在Canvas上绘制出一个红色正方形和一个圆形。现在让我们来重置这个Canvas:
canvas.attr("width", canvas.width());
canvas.attr("height", canvas.height());
现在,将下面这行代码添加到重置宽度/高度的后面:
context.fillRect(40, 40, 100, 100);
这应该会绘制出一个红色正方形,对吗?(记住:之前设置了fillStyle属性。)那么,为什么它实际上绘制出了一个黑色正方形呢?
重新设置宽度,高度技巧的缺点是,它会完全重置Canvas上的所有内容,包括样式和颜色。所以,只有准备完全重置Canvas,而不仅仅是清除内容时,你才能使用这种方法。
保存和恢复绘图状态
前面的例子中我们经常在各种样式之间切换,甚至有时候会在不同颜色之间反复切换。这种重复是很麻烦的,它意味着如果你想要返回之前使用的一些样式,必须重写大量的代码。 幸好,画布能够记住一些样式和属性,这样将来你就可以再次使用。这就是所谓的保存和恢复画布绘图状态。然而,问题是,如果要记住多个状态,操作起来可能令人困惑,因为你必须跟踪所有发生的变化。
画布绘图状态是什么
在画布中,绘图状态指的是描述某一时刻2D渲染上下文外观的整套属性,从简单的颜色值到复杂的变换矩阵(transformation matrix)及其他特性。
用于描述画布绘图状态的全部属性为:变换矩阵、裁剪区域(clipping region)、globalAlpha、globalCompositeOperation、strokeStyle、fillStyle、lineWidth、lineCap、lineJoin、miterLimit、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、font、textAlign和textBaseline。
有一点很重要,画布上的当前路径和当前位图(正在显示的内容)并不属于状态。我们更应该将状态看做2D渲染上下文属性的描述,而不是画布上显示的所有内容的副本。
保存绘图状态
保存画布状态非常简单。你需要做的就是调用2D渲染上下文的save
方法。仅此而已。save方法的代码如下:
var canvas = $("#myCanvas");
var context = canvas.get(0).getContext("2d");
context.fillStyle = "rgb(255, 0, 0)";
context.save(); // Save the canvas state
context.fillRect(50, 50, 100, 100); // Red square
那么,当你保存绘图状态时,实际上发生了什么呢?可以肯定的是,它必须保存在某个地方。2D渲染上下文会保存一个绘图状态栈,实际上它是一组之前保存的状态,其中最近保存的状态位于顶部——就像一叠纸。绘图状态的默认栈是空的,调用save方法,就会有一个新状态被放入(添加到)这个栈。这意味着,你完全可以多次调用save方法,将多个绘图状态逐一保存到栈中,其中最早的状态在底部。然而,这其中有一点不易理解,那就是你无法将任何绘图状态后移,因为这个过程是有严格顺序的。我们先来了解一下如何访问刚刚保存的状态。
恢复绘图状态
访问一个已有绘图状态与保存它一样简单,唯一的区别是这次调用的是restore
。现在,如果你绘制另一个正方形,并且这次将fillStyle设置为蓝色,那么很快会看到画布绘图状态的好处:
context.fillStyle = "rgb(0, 0, 255)";
context.fillRect(200, 50, 100, 100); // Blue square
这里并没有执行任何特殊操作,唯一修改的是填充颜色。
但是,如果你想换回之前使用的红色填充颜色,该怎么做呢?你无需再次重写fillStyle属性并将它设置为红色,因为前面你将颜色设置为红色之后保存了绘图状态,所以它已经存在于栈中了,你只需要在现有代码之前调用restore,就可以恢复原先的状态:
context.restore(); // Restore the canvas state
context.fillRect(350, 50, 100, 100); // Red square
通过调用restore方法,你能够自动取出最后添加到栈中的绘图状态,并将它应用于2D渲染上下文,用所保存的状态覆盖全部现有的样式。这意味着,虽然你没有在代码中直接修改fillStyle属性,但是它将取得所保存的绘图状态的值——它会变成红色(参见图1)。如果只是修改颜色,效果可能还不够明显,但这个概念适用于所有能够保存到绘图状态中的画布属性。
### 保存和恢复多个绘图状态
在本节开头,我曾提到过一次处理多个状态有一些复杂。但是,在学完前面的内容之后,我希望现在你已经理解该如何处理它了。实话说,如果理解了栈的概念,并且明白新增的项被添加到栈的顶部,并且它们是从栈顶部取回的,那么你就不会觉得它复杂了。栈实际上采用一种后进先出的机制,最近保存到栈的绘图状态将是后来第一个恢复的状态。
如果你修改前面的例子,在将fillStyle设置为蓝色后保存绘图状态,就会明白我的意思:
context.fillStyle = "rgb(255, 0, 0)";
context.save();
context.fillRect(50, 50, 100, 100); // Red square
context.fillStyle = "rgb(0, 0, 255)";
context.save();
context.fillRect(200, 50, 100, 100); // Blue square
context.restore();
context.fillRect(350, 50, 100, 100); // Blue square
第三个正方形现在不是红色,而是蓝色。这是因为最后保存到栈的绘图状态是蓝色的fillStyle,所以它最先恢复
另一个状态是红色的fillStyle.它仍然在栈中等待,你只需要再调用一次restore就能够恢复这个状态:
context.restore();
context.fillRect(50, 200, 100, 100); // Red square
这会从栈返回最后一个状态,并将它删除,使栈变成空的(参见图3)。
变形记
平移 transition
最基本的操作就是平移,即将2D渲染上下文的原点从一个位置移动到另一个位置。在画布中进行平移使用的是translate
方法时,实际上它移动的是2D渲染上下文的坐标原点,而不是所绘制的对象
Translate方法的调用方式如下:
context.translate(150, 150);
两个参数是(x,y)坐标值,表示把2D渲染上下文的原点移动多远.一定要注意,将来你所指定(x,y)坐标值会加上原点的平移,原点最初的默认值是(0,0)。例如,如果执行两次与上面例子完全相同的平移,那么实际上是将原点在x轴方向移动300个单位(0+150+150),在y轴方向也移动300个单位(0+150+150)。
通过移动2D渲染上下文的原点,画布中的所有对象都将移动相应的距离:
context.fillRect(150, 150, 100, 100);
context.translate(150, 150);
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(150, 150, 100, 100);
一般情况下,第二次调用fillRect时,所绘制的正方形的原点坐标是(150,150),但是由于执行了一次平移,这个正方形的原点现在变成(300,300)
一定要理解这其中的原理。红色正方形的原点仍然为(150,150),它只是看上去又平移了150像素,这是因为在黑色正方形绘制之后,2D渲染上下文的原点已经平移了150像素。如果你希望红色正方形仍然出现在点(150,150)原来的位置(即黑色正方形所在位置),那么可以直接将它的原点设为(0,0):
context.translate(150, 150);
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(0, 0, 100, 100);
这是因为你已经将2D渲染上下文移动到位置(150,150),所以从现在开始,所有在点(0,0)绘制的图形实际上都显示在点(150,150)上。
缩放 scale
缩放(scale),顾名思义,它是调整2D渲染上下文的尺寸。它与平移的区别在于(x,y)参数是缩放倍数,而不是像素值。
context.scale(2, 2);
context.fillRect(0, 0, 100, 100);
这个例子将2D渲染上下文的x和y方向都乘以2。通俗地说,2D渲染上下文及其绘制的所有对象现在都变成2倍尺寸
单独使用scale将使所有绘图内容变大,而且它也会使一些对象被画在一些不恰当的位置上。例如,放大2倍实际上意味着现在1个像素变成2个像素.所以如果你绘制了一个x为150像素的图形,现在它看起来像是变成x为300像素了。如果这不符合你的要求,或者你只想要缩放一个图形,可以组合使用scale和translate方法。
context.save();
context.translate(150, 150);
context.scale(2, 2);
context.fillRect(0, 0, 100, 100);
在这个例子中,首先保存画布的状态,再将原点平移到(150,150)。然后,将画布放大两倍,在位置(0,0)绘制一个正方形。因为已经将2D渲染上下文平移到(150,150),所以这个正方形会被绘制在正确的位置,并同时放大两倍
问题是,从现在开始绘制的其他图形都将平移150像素并在两个方向同时放大两倍。幸好,你已经完成了前面一半的工作:在执行变形之前保存了绘图状态。剩下一半工作是恢复之前保存的绘图状态。
context.restore();
context.fillRect(0, 0, 100, 100);
在恢复绘图状态之后,后面绘制的所有图形都不会出现变形效果(参见图5)。
旋转 rotate
如果要我选择一个最喜欢的变形功能,我肯定会选择rotate方法。到现在为止,我们介绍的变形方法的共同特点是它们都很容易调用。rotate方法也不例外,你只需要传入以弧度为单位的2D渲染上下文旋转角度值即可:
context.rotate(0.7854); // Rotate 45 degrees (Math.PI/4)
context.fillRect(0, 0, 100, 100);
然而,这个旋转的结果可能并不是你所期望的。为什么正方形会旋转到浏览器边界以外呢?
出现这种结果,是因为rotate方法是把2D渲染上下文绕其原点(0,0)进行旋转的,在前面这个例子中,原点是屏幕的左上角。因此,你所绘制的正方形本身是不会旋转的,它现在实际上是以45度角绘制到画布中。图7可以帮助理解这一点。
旋转后的2D绘图上下文
当然,如果你只想旋转所要绘制的图形,那么这样肯定不行。这时,仍然还需要使用translate方法。要实现所期望的效果,需要将2D渲染上下文的原点平移到正在绘制的图形的中心。然后,再对画布执行一次旋转,接着在当前位置绘制图形。这个过程描述起来有些复杂,所以让我们用示例代码来演示这个过程:
context.translate(200, 200); // Translate to centre of square
context.rotate(0.7854); // Rotate 45 degrees
context.fillRect(-50, -50, 100, 100); // Draw a square with the centre at the point of rotation
这样你会得到一个旋转45度角的有趣正方形,它正位于你想要的位置(参见图8)。
阴影特效
所有人都喜欢好看的阴影效果,它们可能是Adobe Photoshop中使用最广泛的效果了,并且也经常在Web和图形设计中使用。如果操作正确,它们实际上确实能够增加图像真实感。然而,如果操作不当,它们也可能完全毁掉一个图像。
在画布中创建阴影效果是相对较简单的,它可以通过4个全局属性进行控制。这些属性是shadowBlur、shadowOffsetX、shadowOffsetY和shadowColor。我们马上开始逐一讲解这些属性。
默认情况下,2D渲染上下文是不会绘制阴影效果的,因为shadowBlur、shadowOffsetx和shadowOffsetY都设置为0,而shadowColor设置为透明黑色。
创建阴影效果的唯一方法是将shadowColor修改为不透明值,同时将shadowBlur、shadowOffsetX或shadowOffsetY都设置为非0值:
context.shadowBlur = 20;
context.shadowColor = "rgb(0, 0, 0)";
context.fillRect(50, 50, 100, 100);
在这个例子中,给阴影设置了20像素的模糊值,并将它的颜色设置为完全不透明的黑色。阴影的偏移值在x轴和y轴方向仍然保持为默认值0。需要特别指出的是,即使使用了不透明的黑色,但由于采用了模糊效果,这个阴影在边界上仍然有些透明效果
修改shadowBlur,shadowoffsetX或shadowOffsetY属性,就能够创建不同的阴影效果:
context.shadowBlur = 0;
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = "rgba(100, 100, 100, 0.5)"; // Transparent grey
context.fillRect(200, 50, 100, 100);
将模糊修改为0,创建清晰的阴影效果,而稍微向右下偏移,就得到一个不同的阴影效果。使用rgba颜色值将shadowColor设置为透明浅灰色,就能够实现更炫的效果
画布的阴影支持所有图形,所以完全可以在所绘制的圆形或其他图形上创建阴影效果。甚至可以将颜色修改为任意奇特的值:
context.shadowColor = "rgb(255, 0, 0)"; // Red
context.shadowBlur = 50;
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.beginPath();
context.arc(400, 100, 50, 0, Math.PI*2, false);
context.closePath();
context.fill();
这段代码会得到一个非常漂亮的圆形,它后面有一个鲜红色阴影效果
复杂路径
多边形
画布中的路径不仅限于线条和圆,实际上还可以用它们来创建各种奇妙的图形。
单独一条直线路径可以绘制成一条漂亮的线条,下面介绍如何将多个路径连接在一起。
context.beginPath();
context.moveTo(100, 50);
context.lineTo(150, 150);
context.lineTo(50, 150);
context.closePath();
context.stroke();
context.fill();
你应该能够读懂所有这些代码—一它先开始一条路径,将原点移到当前路径,从当前路径原点绘制一条线到一个指定点,再绘制一条线到另一个点,然后再继续,那么我们在这里做了什么呢?我们刚刚做的就是将多个路径连接在一起,我们只需要不停地画线。这确实非常简单,每次调用moveTo或lineTo都会给所谓子路径(subpath)增加一个相应的(x,y)坐标值。事实上,moveTo
会创建一条全新的子路径,而lineTo
只是沿着一条已有的子路径继续画线。这条子路径会记录我们所添加的最后一个坐标值,因而可以连续多次调用lineTo
方法。lineTo的每次调用都是从子路径的最后一个坐标值(由moveTo或lineTo调用留下)开始画线,绘制一条线连接lineTo参数所定义的坐标值,然后再将子路径更新到新的坐标值。
绘制三角形的最后一步是调用closePath
方法,它会画一条线连接子路径的最后一个点和第一个点——封闭路径。它也会将起点和终点添加到子路径上,这两个点现在具有相同的坐标值
贝塞尔曲线
要在画布中绘制一条曲线,我们可以使用arc
方法或arcTo
方法(在两点间绘制一条弧线),但是这些弧线只是一条具有相同半径的曲线。要创建一条更复杂的曲线,需要使用贝塞尔曲线(Bézier curve)方法:quadraticCurveTo
或bezierCurveTo
。
这两种贝塞尔曲线都是通过控制点将一条直线变成曲线。二次贝塞尔曲线只有一个控制点,这意味着线条中只有一次弯曲;而三次贝塞尔曲线则有两个控制点,这意味着一条线中会有两次弯曲。通过图2,我们可以直观地了解这两种曲线的效果,左边是二次贝塞尔曲线,右边是三次贝塞尔曲线。
要在画布中创建这两种曲线,只需要直接调用quadraticCurveTo或bezierCurveTo。先尝试使用
context.lineWidth = 5;
context.beginPath();
context.moveTo(50, 250);
context.quadraticCurveTo(250, 100, 450, 250);
context.stroke();
除了quadraticCurveTo
方法,这段代码的其他语句你都理解。这个方法有4个参数:控制点的(x,y)坐标值(图2中的cpx和cpy),以及路径目标点的(x,y)坐标。控制点在水平(x)方向上位于线条的中心,在垂直(y)方向上稍微偏上(如图2所示),图3所示的就是这条漂亮的曲线。
创建三次贝塞尔曲线也很简单:
context.lineWidth = 5;
context.beginPath();
context.moveTo(50, 250);
context.bezierCurveTo(150, 50, 350, 450, 450, 250);
context.stroke();
bezierCurveTo
函数有6个参数:第一个控制点的(x,y)坐标值(如图2中的cplx和cply),第二个控制点(x,y)坐标值(cp2x和cp2y),以及路径目标点的(x,y)坐标。两个控制点都位于如图2所示的位置,屏幕上显示了两次弯曲的曲线
现在绘制的贝塞尔曲线效果已经很不错了,但是在实践中它们一般不单独使用。贝塞尔曲线的最大用处是组合及附加到其他路径上,从而创建一些非常复杂的图形。你不再受限于只能创建直线和简单弧线的路径了!
将画布图像数据保存为一个HTML图像元素
context.save();
context.fillRect(50, 50, 100, 100);
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(100, 100, 100, 100);
context.restore();
context.fillRect(150, 150, 100, 100);
var dataURL = canvas.get(0).toDataURL();
var dataURL = canvas.get(0).toDataURL();
var img = $("<img></img>");
img.attr("src", dataURL);
canvas.replaceWith(img);
读取图片
从外部读取已有的图像文件到Canvas中
前面的章节讲述了如何将画布导出为图像,将它保存到本地和与他人共享。现在,我们将学习如何实现完全相反的操作:将图像加载到画布中。介绍这个功能的主要原因是,它使我们能够用2D渲染上下文方法对原本不是在画布中创建的图像进行处理。我们还可以使用几种特殊的像素处理方法,对图像执行一些有趣的特殊操作。
将图像加载到画布中实际上与绘制图像一样简单——只涉及一个方法。在调用drawlmage方法时,至少需要三个参数:所绘制的图像和图像绘制位置的(x,y)坐标。这个方法的完整形式是:
context.drawImage(image, x, y);
参数image可以是HTML img元素、HTML5 canvas元素或HTML5 video元素。不仅局限于图像让画布的前景更加光明。后面我们还将学习如何在画布中使用和操作HTML5 video元素。
首先,让我们使用与HTML文件位于相同目录的一个图像(如图1所示),将一个HTML img元素绘制到画布中。
var image = new Image();
image.src = "example.jpg";
$(image).load(function()
{
context.drawImage(image, 0, 0);
});
这里所做的第一件事是使用Image类为HTML img元素赋一个空的DOM对象。 然后,通过把它的src属性设置为一个有效的图像文件路径,就可以将该图像加载到图像元素中, 这就好像是设置了HTML img元素的src属性。实际上这创建了一个普通的HTML img元素,但是并没有将它显示在浏览器上。 如果只希望给画布传递一个图像,而实际上不将它添加到HTML代码中,那么就可以使用这种方法。 如果你就是想要看到这个HTML图像,那么完全可以跳过这些步骤,将image变量的值赋给现有HTML img元素的DOM对象。
最后要做的一件事是将这个图像对象传递给2D渲染上下文的drawlmage方法,但是在这之前,我们需要确认这个图像已经完全加载。 为此,可以使用jQuery的load方法,它是在一个元素完全加载后触发load事件时调用的方法,load事件只有当所有内容(包括图像)完成加载之后才会触发。 直接将jQuery的load方法赋给图像对象,就可以保证我们只需要等待这个图像的加载完成,而不需要等待其他内容的加载。
现在,我们知道这个图像在什么时候完成加载,我们将drawlmage方法置于load事件被触发之后运行的回调事件中。drawlmage方法的参数就是刚刚创建的图像对象,以及绘制图像的原点(x,y)坐标值。 如果一切正常,我们就能够将图像绘制到画布上,尽管图像被剪掉一部分(参见图1)。然而,不需要担心,因为剪掉的原因是画布小于所绘制的图像尺寸,而图像是以完整尺寸绘制的。
在这个例子中,画布的宽高分别为300px和220px,而示例图像宽度为394px,高度为284px。图像超出画布的部分不可见。下一章节,我们将介绍一些方法来让图像适应画布尺寸。