一、 基本的画布功能
创建 <canvas> 元素时至少要设置其 width 和 height 属性,这样才能告诉浏览器在多大面积上绘
图。出现在开始和结束标签之间的内容是后备数据,会在浏览器不支持 <canvas> 元素时显示。比如:
html
<canvas id="drawing" width="200" height="200">A drawing of something.</canvas>
与其他元素一样,width 和 height 属性也可以在 DOM 节点上设置,因此可以随时修改。整个元 素还可以通过 CSS 添加样式,并且元素在添加样式或实际绘制内容前是不可见的。
要在画布上绘制图形,首先要取得绘图上下文。使用 getContext() 方法可以获取对绘图上下文的 引用。对于平面图形,需要给这个方法传入参数"2d" ,表示要获取 2D 上下文对象:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 其他代码
}
使用 <canvas> 元素时,最好先测试一下 getContext() 方法是否存在。有些浏览器对 HTML 规范
中没有的元素会创建默认 HTML 元素对象。这就意味着即使 drawing 包含一个有效的元素引用,
getContext() 方法也未必存在。
可以使用 toDataURL() 方法导出 <canvas> 元素上的图像。这个方法接收一个参数:要生成图像 的 MIME 类型(与用来创建图形的上下文无关)。例如,要从画布上导出一张 PNG 格式的图片,可以这 样做:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
// 取得图像的数据 URI
let imgURI = drawing.toDataURL("image/png");
// 显示图片
let image = document.createElement("img");
image.src = imgURI;
document.body.appendChild(image);
}
浏览器默认将图像编码为 PNG 格式,除非另行指定。Firefox 和 Opera 还支持传入"image/jpeg"
进行 JPEG 编码。因为这个方法是后来才增加到规范中的,所以支持的浏览器也是在后面的版本实现的, 包括 IE9 、 Firefox 3.5 和 Opera 10 。
注意 如果画布中的图像是其他域绘制过来的, toDataURL()方法就会抛出错误。
二、2D 绘图上下文
2D 绘图上下文提供了绘制 2D 图形的方法,包括矩形、弧形和路径。 2D 上下文的坐标原点 (0, 0) 在 <canvas>元素的左上角。所有坐标值都相对于该点计算,因此 x 坐标向右增长, y 坐标向下增长。默认 情况下,width 和 height 表示两个方向上像素的最大值。
1 填充和描边
2D 上下文有两个基本绘制操作:填充和描边。填充以指定样式(颜色、渐变或图像)自动填充形
状,而描边只为图形边界着色。大多数 2D 上下文操作有填充和描边的变体,显示效果取决于两个属性: fillStyle 和 strokeStyle 。
这两个属性可以是字符串、渐变对象或图案对象,默认值都为 "#000000" 。字符串表示颜色值,可
以是 CSS 支持的任意格式:名称、十六进制代码、 rgb 、 rgba 、 hsl 或 hsla 。比如:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
context.strokeStyle = "red";
context.fillStyle = "#0000ff";
}
这里把 strokeStyle 设置为"red"(CSS 颜色名称),把 fillStyle 设置为"#0000ff"(蓝色)。
所有与描边和填充相关的操作都会使用这两种样式,除非再次修改。这两个属性也可以是渐变或图案, 本章后面会讨论。
2 绘制矩形
矩形是唯一一个可以直接在 2D 绘图上下文中绘制的形状。
与绘制矩形相关的方法有 3 个: fillRect()、 strokeRect() 和 clearRect() 。
这些方法都接收 4 个参数:矩形 x 坐标、矩形 y 坐标、 矩形宽度和矩形高度。这几个参数的单位都是像素。
fillRect()方法
用于以指定颜色在画布上绘制并填充矩形。填充的颜色使用 fillStyle 属性指
定。来看下面的例子:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制半透明蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.fillRect(30, 30, 50, 50);
}
以上代码先将 fillStyle 设置为红色并在坐标点(10, 10)绘制了一个宽高均为 50 像素的矩形。接
着,使用 rgba() 格式将 fillStyle 设置为半透明蓝色,并绘制了另一个与第一个部分重叠的矩形。
结果就是可以透过蓝色矩形看到红色矩形。
strokeRect()方法
使用通过 strokeStyle 属性指定的颜色绘制矩形轮廓。下面是一个例子:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 绘制红色轮廓的矩形
context.strokeStyle = "#ff0000";
context.strokeRect(10, 10, 50, 50);
// 绘制半透明蓝色轮廓的矩形
context.strokeStyle = "rgba(0,0,255,0.5)";
context.strokeRect(30, 30, 50, 50);
}
以上代码同样绘制了两个重叠的矩形,不过只有轮廓,而不是实心的
注意 描边宽度由 lineWidth 属性控制,它可以是任意整数值。类似地, lineCap 属性控
制线条端点的形状[ "butt" (平头)、 "round" (出圆头)或 "square" (出方头)],而 lineJoin
属性控制线条交点的形状[ "round" (圆转)、 "bevel" (取平)或 "miter" (出尖)]。
clearRect()方法
可以擦除画布中某个区域。该方法用于把绘图上下文中的某个区域变透明。
通过先绘制形状再擦除指定区域,可以创建出有趣的效果,比如从已有矩形中开个孔。来看下面的例子:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制半透明蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.fillRect(30, 30, 50, 50);
// 在前两个矩形重叠的区域擦除一个矩形区域
context.clearRect(40, 40, 10, 10);
}
以上代码在两个矩形重叠的区域上擦除了一个小矩形 。
3 绘制路径
2D 绘图上下文支持很多在画布上绘制路径的方法。通过路径可以创建复杂的形状和线条。要绘制
路径,必须首先调用 beginPath() 方法以表示要开始绘制新路径。然后,再调用下列方法来绘制路径。
arc(x, y, radius, startAngle, endAngle, counterclockwise)
以坐标 (x, y) 为圆 心,以 radius 为半径绘制一条弧线,起始角度为 startAngle ,结束角度为 endAngle (都是 弧度)。最后一个参数 counterclockwise 表示是否逆时针计算起始角度和结束角度(默认为 顺时针)。
arcTo(x1, y1, x2, y2, radius)
以给定半径 radius ,经由 (x1, y1) 绘制一条从上一点 到(x2, y2) 的弧线。
bezierCurveTo(c1x, c1y, c2x, c2y, x, y)
以 (c1x, c1y) 和 (c2x, c2y) 为控制点, 绘制一条从上一点到(x, y) 的弧线(三次贝塞尔曲线)。
lineTo(x, y)
绘制一条从上一点到 (x, y) 的直线。
moveTo(x, y)
不绘制线条,只把绘制光标移动到 (x, y) 。
quadraticCurveTo(cx, cy, x, y)
以 (cx, cy) 为控制点,绘制一条从上一点到 (x, y) 的弧线(二次贝塞尔曲线)。
rect(x, y, width, height)
以给定宽度和高度在坐标点 (x, y) 绘制一个矩形。这个方法 与 strokeRect() 和 fillRect() 的区别在于,它创建的是一条路径,而不是独立的图形。
创建路径之后,可以使用 closePath() 方法绘制一条返回起点的线。如果路径已经完成,则既可
以指定 fillStyle 属性并调用 fill() 方法来填充路径,也可以指定 strokeStyle 属性并调用
stroke() 方法来描画路径,还可以调用 clip() 方法基于已有路径创建一个新剪切区域。
下面这个例子使用前面提到的方法绘制了一个不带数字的表盘:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 创建路径
context.beginPath();
// 绘制外圆
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 绘制内圆
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 绘制分针
context.moveTo(100, 100);
context.lineTo(100, 15);
// 绘制时针
context.moveTo(100, 100);
context.lineTo(35, 100);
// 描画路径
context.stroke();
}
这个例子使用 arc() 绘制了两个圆形,一个外圆和一个内圆,以构成表盘的边框。外圆半径 99 像
素,原点为 (100,100) ,也就是画布的中心。要绘制完整的圆形,必须从 0 弧度绘制到 2π 弧度(使用数 学常量 Math.PI )。而在绘制内圆之前,必须先把路径移动到内圆上的一点,以避免绘制出多余的线条。 第二次调用 arc() 时使用了稍小一些的半径,以呈现边框效果。然后,再组合运用 moveTo() 和 lineTo() 分别绘制分针和时针。最后一步是调用 stroke() ,得到如图 所示的图像。
路径是 2D 上下文的主要绘制机制,为绘制结果提供了很多控制。因为路径经常被使用,所以也有 一个 isPointInPath() 方法,接收 x 轴和 y 轴坐标作为参数。这个方法用于确定指定的点是否在路径 上,可以在关闭路径前随时调用,比如:
javascript
if (context.isPointInPath(100, 100)) {
alert("Point (100, 100) is in the path.");
}
2D 上下文的路径 API 非常可靠,可用于创建涉及各种填充样式、描述样式等的复杂图像。
4 绘制文本
文本和图像混合也是常见的绘制需求,因此 2D 绘图上下文还提供了绘制文本的方法,即 fillText()
和 strokeText() 。这两个方法都接收 4 个参数:要绘制的字符串、 x 坐标、 y 坐标和可选的最大像素
宽度。而且,这两个方法最终绘制的结果都取决于以下 3 个属性。
font
以 CSS 语法指定的字体样式、大小、字体族等,比如 "10px Arial" 。
textAlign
指定文本的对齐方式,可能的值包括 "start" 、 "end" 、 "left" 、 "right" 和 "center"。推荐使用 "start" 和 "end" ,不使用 "left" 和 "right" ,因为前者无论在从左到右书写的语言还是从右到左书写的语言中含义都更明确。
textBaseLine
指定文本的基线,可能的值包括 "top" 、 "hanging" 、 "middle" 、
"alphabetic" 、 "ideographic" 和 "bottom" 。
这些属性都有相应的默认值,因此没必要每次绘制文本时都设置它们。
fillText() 方法使用 fillStyle 属性绘制文本,而 strokeText() 方法使用 strokeStyle 属性。通常, fillText() 方法 是使用最多的,因为它模拟了在网页中渲染文本。例如,下面的例子会在前一节示例的表盘顶部绘制数
字" 12 ":
javascript
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
因为把 textAlign 设置为了 "center" ,把 textBaseline 设置为了 "middle" ,所以 (100, 20) 表
示文本水平和垂直中心点的坐标。如果 textAlign 是 "start" ,那么 x 坐标在从左到右书写的语言中
表示文本的左侧坐标,而 "end" 会让 x 坐标在从左到右书写的语言中表示文本的右侧坐标。例如:
javascript
// 正常
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
// 与开头对齐
context.textAlign = "start";
context.fillText("12", 100, 40);
// 与末尾对齐
context.textAlign = "end";
context.fillText("12", 100, 60);
22
字符串 "12" 被绘制了 3 次,每次使用的坐标都一样,但 textAlign 值不同。为了让每个字符串不
至于重叠,每次绘制的 y 坐标都会设置得大一些。
因为表盘中垂直的线条是居中的,所以文本的对齐方式就一目了然了。类似地,通过修改 textBaseline 属性,可以改变文本的垂直对齐方式。比如,设置为"top" 意味着 y 坐标表示文本顶部, "bottom" 表示 文本底部,"hanging" 、 "alphabetic" 和 "ideographic" 分别引用字体中特定的基准点。
由于绘制文本很复杂,特别是想把文本绘制到特定区域的时候,因此 2D 上下文提供了用于辅助确
定文本大小的 measureText() 方法。这个方法接收一个参数,即要绘制的文本,然后返回一个
TextMetrics 对象。这个返回的对象目前只有一个属性 width ,不过将来应该会增加更多度量指标。
measureText() 方法使用 font 、 textAlign 和 textBaseline 属性当前的值计算绘制指定文本
后的大小。例如,假设要把文本 "Hello world!" 放到一个 140 像素宽的矩形中,可以使用以下代码,
从 100 像素的字体大小开始计算,不断递减,直到文本大小合适:
javascript
let fontSize = 100;
context.font = fontSize + "px Arial";
while(context.measureText("Hello world!").width > 140) {
fontSize--;
context.font = fontSize + "px Arial";
}
context.fillText("Hello world!", 10, 10);
context.fillText("Font size is " + fontSize + "px", 10, 50);
fillText() 和 strokeText() 方法还有第四个参数,即文本的最大宽度。这个参数是可选的
( Firefox 4 是第一个实现它的浏览器),如果调用 fillText() 和 strokeText() 时提供了此参数,但要
绘制的字符串超出了最大宽度限制,则文本会以正确的字符高度绘制,这时字符会被水平压缩,以达到 限定宽度。
绘制文本是一项比较复杂的操作,因此支持 <canvas> 元素的浏览器不一定全部实现了相关的文本
绘制 API 。
5 变换
上下文变换可以操作绘制在画布上的图像。 2D 绘图上下文支持所有常见的绘制变换。在创建绘制
上下文时,会以默认值初始化变换矩阵,从而让绘制操作如实应用到绘制结果上。对绘制上下文应用变 换,可以导致以不同的变换矩阵应用绘制操作,从而产生不同的结果。
以下方法可用于改变绘制上下文的变换矩阵。
rotate(angle)
围绕原点把图像旋转 angle 弧度。
scale(scaleX, scaleY)
通过在 x 轴乘以 scaleX 、在 y 轴乘以 scaleY 来缩放图像。 scaleX 和 scaleY 的默认值都是 1.0 。
translate(x, y)
把原点移动到 (x, y) 。执行这个操作后,坐标 (0, 0) 就会变成 (x, y) 。
transform(m1_1, m1_2, m2_1, m2_2, dx, dy) :像下面这样通过矩阵乘法直接修改矩阵。
m1_1 m1_2 dx
m2_1 m2_2 dy
0 0 1
setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy)
把矩阵重置为默认值,再以传入的 参数调用 transform() 。
变换可以简单,也可以复杂。
例如,在前面绘制表盘的例子中,如果把坐标原点移动到表盘中心, 那再绘制表针就非常简单了:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 创建路径
context.beginPath();
// 绘制外圆
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 绘制内圆
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 移动原点到表盘中心
context.translate(100, 100);
// 绘制分针
context.moveTo(0, 0);
context.lineTo(0, -85);
// 绘制时针
context.moveTo(0, 0);
context.lineTo(-65, 0);
// 描画路径
context.stroke();
}
把原点移动到(100, 100),也就是表盘的中心后,要绘制表针只需简单的数学计算即可。这是因为所 有计算都是基于(0, 0),而不是(100, 100)了。当然,也可以使用 rotate()方法来转动表针
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 创建路径
context.beginPath();
// 绘制外圆
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 绘制内圆
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 移动原点到表盘中心
context.translate(100, 100);
// 旋转表针
context.rotate(1);
// 绘制分针
context.moveTo(0, 0);
context.lineTo(0, -85);
// 绘制时针
context.moveTo(0, 0);
context.lineTo(-65, 0);
// 描画路径
context.stroke();
}
因为原点已经移动到表盘中心,所以旋转就是以该点为圆心的。这相当于把表针一头固定在表盘中
心,然后向右拨了一个弧度。
所有这些变换,包括 fillStyle 和 strokeStyle 属性,会一直保留在上下文中,直到再次修改
它们。虽然没有办法明确地将所有值都重置为默认值,但有两个方法可以帮我们跟踪变化。如果想着什 么时候再回到当前的属性和变换状态,可以调用 save() 方法。调用这个方法后,所有这一时刻的设置 会被放到一个暂存栈中。保存之后,可以继续修改上下文。而在需要恢复之前的上下文时,可以调用 restore()方法。这个方法会从暂存栈中取出并恢复之前保存的设置。多次调用 save() 方法可以在暂 存栈中存储多套设置,然后通过 restore() 可以系统地恢复。下面来看一个例子:
javascript
context.fillStyle = "#ff0000";
context.save();
context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); // 在(100, 100)绘制蓝色矩形
context.restore();
context.fillRect(10, 10, 100, 200); // 在(100, 100)绘制绿色矩形
context.restore();
context.fillRect(0, 0, 100, 200); // 在(0, 0)绘制红色矩形
以上代码先将 fillStyle 设置为红色,然后调用 save()。接着,将 fillStyle 修改为绿色,坐
标移动到 (100, 100) ,并再次调用 save() ,保存设置。随后,将 fillStyle 属性设置为蓝色并绘制一
个矩形。因为此时坐标被移动了,所以绘制矩形的坐标实际上是 (100, 100) 。在调用 restore() 之后, fillStyle 恢复为绿色,因此这一次绘制的矩形是绿色的。而绘制矩形的坐标是 (110, 110) ,因为变换 仍在起作用。再次调用 restore() 之后,变换被移除, fillStyle 也恢复为红色。绘制最后一个矩形 的坐标变成了(0, 0) 。
注意, save() 方法只保存应用到绘图上下文的设置和变换,不保存绘图上下文的内容
6 绘制图像(把现有图像绘制到画布上)
2D 绘图上下文内置支持操作图像。如果想把现有图像绘制到画布上,可以使用 drawImage() 方法。
这个方法可以接收 3 组不同的参数,并产生不同的结果。最简单的调用是传入一个 HTML 的 <img> 元素, 以及表示绘制目标的 x 和 y 坐标,结果是把图像绘制到指定位置。比如:
javascript
let image = document.images[0];
context.drawImage(image, 10, 10);
以上代码获取了文本中的第一个图像,然后在画布上的坐标 (10, 10) 处将它绘制了出来。绘制出来的 图像与原来的图像一样大。如果想改变所绘制图像的大小,可以再传入另外两个参数:目标宽度和目标 高度。这里的缩放只影响绘制的图像,不影响上下文的变换矩阵。比如下面的例子:
javascript
context.drawImage(image, 50, 10, 20, 30);
执行之后,图像会缩放到 20 像素宽、30 像素高。
还可以只把图像绘制到上下文中的一个区域。此时,需要给 drawImage() 提供 9 个参数:要绘制
的图像、源图像 x 坐标、源图像 y 坐标、源图像宽度、源图像高度、目标区域 x 坐标、目标区域 y 坐标、
目标区域宽度和目标区域高度。这个重载后的 drawImage() 方法可以实现最大限度的控制,比如:
javascript
context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60);
最终,原始图像中只有一部分会绘制到画布上。这一部分从(0, 10)开始,50 像素宽、50 像素高。而 绘制到画布上时,会从(0, 100)开始,变成 40 像素宽、60 像素高。
第一个参数除了可以是 HTML 的 <img> 元素,还可以是另一个 <canvas> 元素,这样就会把另一个
画布的内容绘制到当前画布上。
结合其他一些方法, drawImage() 方法可以方便地实现常见的图像操作。操作的结果可以使用
toDataURL() 方法获取。不过有一种情况例外:如果绘制的图像来自其他域而非当前页面,则不能获取 其数据。此时,调用 toDataURL() 将抛出错误。比如,如果来自 www.example.com 的页面上绘制的是 来自 www.wrox.com 的图像,则上下文就是"脏的",获取数据时会抛出错误。
7 阴影
2D 上下文可以根据以下属性的值自动为已有形状或路径生成阴影。
shadowColor
CSS 颜色值,表示要绘制的阴影颜色,默认为黑色。
shadowOffsetX
阴影相对于形状或路径的 x 坐标的偏移量,默认为 0 。
shadowOffsetY
阴影相对于形状或路径的 y 坐标的偏移量,默认为 0 。
shadowBlur
像素,表示阴影的模糊量。默认值为 0 ,表示不模糊。
这些属性都可以通过 context 对象读写。只要在绘制图形或路径前给这些属性设置好适当的值,
阴影就会自动生成。比如:
javascript
let context = drawing.getContext("2d");
// 设置阴影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
这里两个矩形使用了相同的阴影样式,得到了如图 所示的结果。
8 渐变
createLinearGradient线性渐变
渐变通过 CanvasGradient 的实例表示,在 2D 上下文中创建和修改都非常简单。要创建一个新的
线性渐变,可以调用上下文的 createLinearGradient() 方法。这个方法接收 4 个参数:起点 x 坐标、 起点 y 坐标、终点 x 坐标和终点 y 坐标。调用之后,该方法会以指定大小创建一个新的 CanvasGradient 对象并返回实例。 有了 gradient 对象后,接下来要使用 addColorStop() 方法为渐变指定色标。这个方法接收两个参数:色标位置和 CSS 颜色字符串。色标位置通过 0 ~ 1 范围内的值表示, 0 是第一种颜色, 1 是最后 一种颜色。比如:
javascript
let gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
这个 gradient 对象现在表示的就是在画布上从(30, 30)到(70, 70)绘制一个渐变。渐变的起点颜色
为白色,终点颜色为黑色。可以把这个对象赋给 fillStyle 或 strokeStyle 属性,从而以渐变填充
或描画绘制的图形:
javascript
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
为了让渐变覆盖整个矩形,而不只是其中一部分,两者的坐标必须搭配合适。以上代码将得到如图
所示的结果。
如果矩形没有绘制到渐变的范围内,则只会显示部分渐变。比如:
javascript
context.fillStyle = gradient;
context.fillRect(50, 50, 50, 50);
以上代码执行之后绘制的矩形只有左上角有一部分白色。这是因为矩形的起点在渐变的中间,此时
颜色的过渡几乎要完成了。结果矩形大部分地方是黑色的,因为渐变不会重复。保持渐变与形状的一致 非常重要,有时候可能需要写个函数计算相应的坐标。比如:
javascript
function createRectLinearGradient(context, x, y, width, height) {
return context.createLinearGradient(x, y, x+width, y+height);
}
这个函数会基于起点的 x、y坐标和传入的宽度、高度创建渐变对象,之后调用 fillRect()方法
时可以使用相同的值:
javascript
let gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
计算坐标是使用画布时重要而复杂的问题。使用类似 createRectLinearGradient()这样的辅助
函数能让计算坐标简单一些。
createRadialGradient径向渐变
径向渐变(或放射性渐变)要使用 createRadialGradient() 方法来创建。这个方法接收 6 个参
数,分别对应两个圆形圆心的坐标和半径。前 3 个参数指定起点圆形中心的 x 、 y 坐标和半径,后 3 个参 数指定终点圆形中心的 x 、 y 坐标和半径。在创建径向渐变时,可以把两个圆形想象成一个圆柱体的两 个圆形表面。把一个表面定义得小一点,另一个定义得大一点,就会得到一个圆锥体。然后,通过移动 两个圆形的圆心,就可以旋转这个圆锥体。
要创建起点圆心在形状中心并向外扩散的径向渐变,需要将两个圆形设置为同心圆。比如,要在前
面例子中矩形的中心创建径向渐变,则渐变的两个圆形的圆心都必须设置为 (55, 55) 。这是因为矩形的起 点是(30, 30) ,终点是 (80, 80) 。代码如下:
javascript
let gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
运行以上代码会得到如图 所示的效果。
因为创建起来要复杂一些,所以径向渐变比较难处理。不过,通常情况下,起点和终点的圆形都是
同心圆,只要定义好圆心坐标,剩下的就是调整各自半径的问题了。
9 图案
图案是用于填充和描画图形的重复图像。要创建新图案,可以调用 createPattern() 方法并传入
两个参数:一个 HTML <img> 元素和一个表示该如何重复图像的字符串。第二个参数的值与 CSS 的 background-repeat 属性是一样的,包括 "repeat" 、 "repeat-x" 、 "repeat-y" 和 "no-repeat" 。
比如:
javascript
<body>
<canvas id="drawing" width="200" height="200">A drawing of something.</canvas>
<img id="image" src="./img.png" alt="Pattern Image" style="display:none;">
</body>
<script>
let drawing = document.getElementById("drawing");
let image = document.getElementById("image");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 等待图片加载完成
image.onload = function() {
// 创建图案填充
let pattern = context.createPattern(image, "repeat");
// 绘制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);
};
}
</script>
记住,跟渐变一样,图案的起点实际上是画布的原点 (0, 0) 。将填充样式设置为图案,表示在指定位 置而不是开始绘制的位置显示图案。以上代码执行的结果如图 所示。
传给 createPattern() 方法的第一个参数也可以是 <video> 元素或者另一个 <canvas>
元素。
10 图像数据
2D 上下文中比较强大的一种能力是可以使用 getImageData() 方法获取原始图像数据。这个方法
接收 4 个参数:要取得数据中第一个像素的左上角坐标和要取得的像素宽度及高度。例如,要从 (10, 5) 开始取得 50 像素宽、 50 像素高的区域对应的数据,可以这样写:
javascript
let imageData = context.getImageData(10, 5, 50, 50);
返回的对象是一个 ImageData 的实例。每个 ImageData 对象都包含 3 个属性:width、height
和 data ,其中, data 属性是包含图像的原始像素信息的数组。每个像素在 data 数组中都由 4 个值表 示,分别代表红、绿、蓝和透明度值。换句话说,第一个像素的信息包含在第 0 到第 3 个值中,比如:
javascript
let data = imageData.data,
red = data[0],
green = data[1],
blue = data[2],
alpha = data[3];
这个数组中的每个值都在 0~255 范围内(包括 0 和 255)。对原始图像数据进行访问可以更灵活地操 作图像。例如,通过更改图像数据可以创建一个简单的灰阶过滤器:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d"),
image = document.images[0],
imageData, data,
i, len, average,
red, green, blue, alpha;
// 绘制图像
context.drawImage(image, 0, 0);
// 取得图像数据
imageData = context.getImageData(0, 0, image.width, image.height);
data = imageData.data;
for (i=0, len=data.length; i < len; i+=4) {
red = data[i];
green = data[i+1];
blue = data[i+2];
alpha = data[i+3];
// 取得 RGB 平均值
average = Math.floor((red + green + blue) / 3);
// 设置颜色,不管透明度
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
// 将修改后的数据写回 ImageData 并应用到画布上显示出来
imageData.data = data;
context.putImageData(imageData, 0, 0);
}
html
<!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>Grayscale Image on Canvas</title>
</head>
<body>
<canvas id="drawing" width="450" height="450">A drawing of something.</canvas>
<img id="image" src="https://img0.baidu.com/it/u=2325089072,2338520041&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500" width="450" height="450" alt="Pattern Image">
</body>
<script>
// 获取 canvas 元素和上下文
const canvas = document.getElementById('drawing');
const ctx = canvas.getContext('2d');
// 创建一个图像对象
const img = new Image();
img.crossOrigin = "Anonymous"; // 允许跨域加载
img.src = "https://img0.baidu.com/it/u=2325089072,2338520041&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500"; // 请替换为你自己的图片路径
// 当图像加载完成后执行
img.onload = function() {
// 将图像绘制到画布上
ctx.drawImage(img, 0, 0);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 遍历图像数据并应用灰阶过滤
for (let i = 0; i < data.length; i += 4) {
const r = data[i]; // 红色
const g = data[i + 1]; // 绿色
const b = data[i + 2]; // 蓝色
// 计算灰阶值(平均值)
const gray = (r + g + b) / 3;
// 将RGB值设置为灰阶值
data[i] = gray; // 红色
data[i + 1] = gray; // 绿色
data[i + 2] = gray; // 蓝色
}
// 将修改后的图像数据放回画布
ctx.putImageData(imageData, 0, 0);
};
</script>
</html>
这个例子首先在画布上绘制了一个图像,然后又取得了其图像数据。for 循环遍历了图像数据中的
每个像素,注意每次循环都要给 i 加上 4 。每次循环中取得红、绿、蓝的颜色值,计算出它们的平均值。 然后再把原来的值修改为这个平均值,实际上相当于过滤掉了颜色信息,只留下类似亮度的灰度信息。 之后将 data 数组重写回 imageData 对象。最后调用 putImageData() 方法,把图像数据再绘制到画 布上。结果就得到了原始图像的黑白版。
当然,灰阶过滤只是基于原始像素值可以实现的其中一种操作。要了解基于原始图像数据还可以实
现哪些操作,可以参考 Ilmari Heikkinen 的文章" Making Image Filters with Canvas "。
注意 只有在画布没有加载跨域内容时才可以获取图像数据。如果画布上绘制的是跨域内
容,则尝试获取图像数据会导致 JavaScript 报错。
11 合成
2D 上下文中绘制的所有内容都会应用两个属性: globalAlpha 和 globalComposition Operation ,
globalAlpha 属性
是一个范围在 0~1 的值(包括 0 和 1 ),用于指定所有绘制内容的透明度,默 认值为 0。如果所有后来的绘制都需要使用同样的透明度,那么可以将 globalAlpha 设置为适当的值, 执行绘制,然后再把 globalAlpha 设置为 0 。比如:
javascript
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 修改全局透明度
context.globalAlpha = 0.5;
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
// 重置
context.globalAlpha = 0;
在这个例子中,蓝色矩形是绘制在红色矩形上面的。因为在绘制蓝色矩形前 globalAlpha 被设置
成了 0.5 ,所以蓝色矩形就变成半透明了,从而可以透过它看到下面的红色矩形。
globalCompositionOperation 属性
表示新绘制的形状如何与上下文中已有的形状融合。这个属 性是一个字符串,可以取下列值。
source-over
默认值,新图形绘制在原有图形上面。
source-in
新图形只绘制出与原有图形重叠的部分,画布上其余部分全部透明。
source-out
新图形只绘制出不与原有图形重叠的部分,画布上其余部分全部透明。
source-atop
新图形只绘制出与原有图形重叠的部分,原有图形不受影响。
destination-over
新图形绘制在原有图形下面,重叠部分只有原图形透明像素下的部分可见。
destination-in
新图形绘制在原有图形下面,画布上只剩下二者重叠的部分,其余部分完全
透明。
destination-out
新图形与原有图形重叠的部分完全透明,原图形其余部分不受影响。
destination-atop
新图形绘制在原有图形下面,原有图形与新图形不重叠的部分完全透明。
lighter
新图形与原有图形重叠部分的像素值相加,使该部分变亮。
copy
新图形将擦除并完全取代原有图形。
xor
新图形与原有图形重叠部分的像素执行"异或"计算。
以上合成选项的含义很难用语言来表达清楚,只用黑白图像也体现不出所有合成的效果。下面来看
一个例子:
javascript
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 设置合成方式
context.globalCompositeOperation = "destination-over";
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
虽然后绘制的蓝色矩形通常会出现在红色矩形上面,但将 globalCompositeOperation 属性的值
修改为 "destination-over" 意味着红色矩形会出现在蓝色矩形上面。
使用 globalCompositeOperation 属性时,一定记得要在不同浏览器上进行测试。不同浏览器在
实现这些选项时可能存在差异。这些操作在 Safari 和 Chrome 中仍然有些问题,可以参考 MDN 文档上的 CanvasRenderingContext2D.globalCompositeOperation,比较它们与 IE 或 Firefox 渲染的差异。
三、 WebGL
WebGL 是画布的 3D 上下文。与其他 Web 技术不同, WebGL 不是 W3C 制定的标准,而是 Khronos Group 的标准。根据官网描述," Khronos Group 是非营利性、会员资助的联盟,专注于多平台和设备下 并行计算、图形和动态媒体的无专利费开放标准"。Khronos Group 也制定了其他图形 API ,包括作为浏 览器中 WebGL 基础的 OpenGL ES 2.0 。
OpenGL 这种 3D 图形语言很复杂,要使用 WebGL 最好熟悉 OpenGL ES 2.0,因为很多概念可以照搬过来。
要了解关于 OpenGL 的更多信息,可以访问 OpenGL 网站。另外,推荐一个 WebGL 教程网站: Learn WebGL 。
注意 定型数组是在 WebGL 中执行操作的重要数据结构。
1 WebGL 上下文
在完全支持的浏览器中, WebGL 2.0 上下文的名字叫 "webgl2" , WebGL 1.0 上下文的名字叫
"webgl1" 。如果浏览器不支持 WebGL ,则尝试访问 WebGL 上下文会返回 null 。在使用上下文之前,
应该先检测返回值是否存在:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let gl = drawing.getContext("webgl");
if (gl){
// 使用 WebGL
}
}
这里把 WebGL context 对象命名为 gl。大多数 WebGL 应用和例子遵循这个约定,因为 OpenGL ES 2.0 方法和值通常以"gl"开头。这样可以让 JavaScript 代码看起来更接近 OpenGL 程序。
2 WebGL 基础
取得 WebGL 上下文后,就可以开始 3D 绘图了。如前所述,因为 WebGL 是 OpenGL ES 2.0 的 Web 版,所以本节讨论的概念实际上是 JavaScript 所实现的 OpenGL 概念。
可以在调用 getContext() 取得 WebGL 上下文时指定一些选项。这些选项通过一个参数对象传入,
选项就是参数对象的一个或多个属性。
alpha :布尔值,表示是否为上下文创建透明通道缓冲区,默认为 true 。
depth :布尔值,表示是否使用 16 位深缓冲区,默认为 true 。
stencil :布尔值,表示是否使用 8 位模板缓冲区,默认为 false 。
antialias :布尔值,表示是否使用默认机制执行抗锯齿操作,默认为 true 。
premultipliedAlpha :布尔值,表示绘图缓冲区是否预乘透明度值,默认为 true 。
preserveDrawingBuffer :布尔值,表示绘图完成后是否保留绘图缓冲区,默认为 false 。
建议在充分了解这个选项的作用后再自行修改,因为这可能会影响性能。
可以像下面这样传入 options 对象:
javascript
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let gl = drawing.getContext("webgl", { alpha: false });
if (gl) {
// 使用 WebGL
}
}
这些上下文选项大部分适合开发高级功能。多数情况下,默认值就可以满足要求。
如果调用 getContext() 不能创建 WebGL 上下文,某些浏览器就会抛出错误。为此,最好把这个
方法调用包装在 try / catch 块中:
javascript
Insert IconMargin [download]
let drawing = document.getElementById("drawing"),gl;
// 确保浏览器支持<canvas>
if (drawing.getContext) {
try {
gl = drawing.getContext("webgl");
} catch (ex) {
// 什么也不做
}
if (gl) {
// 使用 WebGL
} else {
alert("WebGL context could not be created.");
}
}
1. 常量
如果你熟悉 OpenGL ,那么可能知道用于操作的各种常量。这些常量在 OpenGL 中的名字以 GL_ 开 头。在 WebGL 中, context 对象上的常量则不包含 GL_ 前缀。例如, GL_COLOR_BUFFER_BIT 常量在 WebGL 中要这样访问 gl.COLOR_BUFFER_BIT 。 WebGL 以这种方式支持大部分 OpenGL 常量(少数常量不支持)。
2. 方法命名
OpenGL (同时也是 WebGL )中的很多方法会包含相关的数据类型信息。接收不同类型和不同数量 参数的方法,会通过方法名的后缀体现这些信息。表示参数数量的数字(1~4 )在先,表示数据类型的 字符串("f "表示浮点数," i "表示整数)在后。比如, gl.uniform4f() 的意思是需要 4 个浮点数值 参数,而 gl.uniform3i() 表示需要 3 个整数值参数。
还有很多方法接收数组,这类方法用字母" v "( vector )来表示。因此, gl.uniform3iv() 就是要接收一个包含 3 个值的数组参数。在编写 WebGL 代码时,要记住这些约定。
3. 准备绘图
准备使用 WebGL 上下文之前,通常需要先指定一种实心颜色清除 <canvas> 。为此,要调用
clearColor() 方法并传入 4 个参数,分别表示红、绿、蓝和透明度值。每个参数必须是 0~1 范围内的 值,表示各个组件在最终颜色的强度。比如:
javascript
gl.clearColor(0, 0, 0, 1); // 黑色
gl.clear(gl.COLOR_BUFFER_BIT);
以上代码把清理颜色缓冲区的值设置为黑色,然后调用 clear()方法,这个方法相当于 OpenGL
中的 glClear() 方法。参数 gl.COLOR_BUFFER_BIT 告诉 WebGL 使用之前定义的颜色填充画布。通 常,所有绘图操作之前都需要先清除绘制区域。
4. 视口与坐标
绘图前还要定义 WebGL 视口。默认情况下,视口使用整个 <canvas> 区域。要改变视口,可以调用 viewport()方法并传入视口相对于 <canvas> 元素的 x 、 y 坐标及宽度和高度。例如,以下代码表示要 使用整个<canvas> 元素:
javascript
gl.viewport(0, 0, drawing.width,
drawing.height);
这个视口的坐标系统与网页中通常的坐标系统不一样。视口的 x和 y坐标起点(0, 0)表示<canvas>
元素的左下角,向上、向右增长可以用点 (width--1, height--1)定义
知道如何定义视口就可以只使用 <canvas> 元素的一部分来绘图。比如下面的例子:
javascript
// 视口是<canvas> 左下角四分之一区域
gl.viewport(0, 0, drawing.width/2, drawing.height/2);
// 视口是<canvas> 左上角四分之一区域
gl.viewport(0, drawing.height/2, drawing.width/2, drawing.height/2);
// 视口是<canvas> 右下角四分之一区域
gl.viewport(drawing.width/2, 0, drawing.width/2, drawing.height/2);
定义视口的坐标系统与视口中的坐标系统不一样。在视口中,坐标原点 (0, 0) 是视口的中心点。左下 角是(--1, --1) ,右上角是 (1, 1) 。
如果绘图时使用了视口外部的坐标,则绘制结果会被视口剪切。例如,要绘制的形状有一个顶点在
(1, 2) ,则视口右侧的图形会被切掉。
5. 缓冲区
在 JavaScript 中,顶点信息保存在定型数组中。要使用这些信息,必须先把它们转换为 WebGL 缓冲 区。创建缓冲区要调用 gl.createBuffer() 方法,并使用 gl.bindBuffer() 方法将缓冲区绑定到
WebGL 上下文。绑定之后,就可以用数据填充缓冲区了。比如:
javascript
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW);
调用 gl.bindBuffer()将 buffer 设置为上下文的当前缓冲区。然后,所有缓冲区操作都在 buffer 上直接执行。因此,调用 gl.bufferData()虽然没有包含对 buffer 的直接引用,但仍然是 在它上面执行的。上面最后一行代码使用一个 Float32Array(通常把所有顶点信息保存在 Float32Array 中)初始化了 buffer。如果想输出缓冲区内容,那么可以调用 drawElements()方法 并传入 gl.ELEMENT_ARRAY_BUFFER。
gl.bufferData() 方法的最后一个参数表示如何使用缓冲区。这个参数可以是以下常量值。
gl.STATIC_DRAW :数据加载一次,可以在多次绘制中使用。
gl.STREAM_DRAW :数据加载一次,只能在几次绘制中使用。
gl.DYNAMIC_DRAW :数据可以重复修改,在多次绘制中使用。
除非是很有经验的 OpenGL 程序员,否则我们会对大多数缓冲区使用 gl.STATIC_DRAW 。
缓冲区会一直驻留在内存中,直到页面卸载。如果不再需要缓冲区,那么最好调用 gl.deleteBuffer() 方法释放其占用的内存:
gl.deleteBuffer(buffer);
6. 错误
与 JavaScript 多数情况下不同的是,在 WebGL 操作中通常不会抛出错误。必须在调用可能失败的方 法后,调用 gl.getError() 方法。这个方法返回一个常量,表示发生的错误类型。下面列出了这些常量。
gl.NO_ERROR :上一次操作没有发生错误( 0 值)。
gl.INVALID_ENUM :上一次操作没有传入 WebGL 预定义的常量。
gl.INVALID_VALUE :上一次操作需要无符号数值,但是传入了负数。
gl.INVALID_OPERATION :上一次操作在当前状态下无法完成。
gl.OUT_OF_MEMORY :上一次操作因内存不足而无法完成。
gl.CONTEXT_LOST_WEBGL :上一次操作因外部事件(如设备掉电)而丢失了 WebGL 上下文。
每次调用 gl.getError() 方法会返回一个错误值。第一次调用之后,再调用 gl.getError() 可
能会返回另一个错误值。如果有多个错误,则可以重复这个过程,直到 gl.getError() 返 回
gl.NO_ERROR 。如果执行了多次操作,那么可以通过循环调用 getError() :
如果 WebGL 代码没有产出想要的输出结果,那么可以调用几次 getError() ,这样有可能帮你找到问题所在。
7. 着色器
着色器 是 OpenGL 中的另一个概念。 WebGL 中有两种着色器: 顶点着色器 和 片段 (或像素) 着色器 。
顶点着色器用于把 3D 顶点转换为可以渲染的 2D 点。片段着色器用于计算绘制一个像素的正确颜色。
WebGL 着色器的独特之处在于,它们不是 JavaScript 实现的,而是使用一种与 C 或 JavaScript 完全不同 的语言 GLSL ( OpenGL Shading Language )写的。
编写着色器
GLSL 是一种类似于 C 的语言,专门用于编写 OpenGL 着色器。因为 WebGL 是 OpenGL ES 2 的实现,
所以 OpenGL 中的着色器可以直接在 WebGL 中使用。这样也可以让桌面应用更方便地移植到 Web 上。
每个着色器都有一个 main() 方法,在绘制期间会重复执行。给着色器传递数据的方式有两种:
attribute 和 uniform 。 attribute 用于将顶点传入顶点着色器,而 uniform 用于将常量值传入任
何着色器。 attribute 和 uniform 是在 main() 函数外部定义的。在值类型关键字之后是数据类型,
然后是变量名。下面是一个简单的顶点着色器的例子:
javascript
// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章"Get started with WebGL---draw a square"
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
这个顶点着色器定义了一个名为 aVertexPosition 的 attribute。这个 attribute 是一个包含
两项的数组(数据类型为 vec2 ),代表 x 和 y 坐标。即使只传入了两个坐标,顶点着色器返回的值也会 包含 4 个元素,保存在变量 gl_Position 中。这个着色器创建了一个新的包含 4 项的数组( vec4 ), 缺少的坐标会补充上,实际上是把 2D 坐标转换为了 3D 坐标。 片段着色器与顶点着色器类似,只不过是通过 uniform 传入数据。下面是一个片段着色器的例子:
javascript
// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章"Get started with WebGL---draw a square"
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
片段着色器必须返回一个值,保存到变量 gl_FragColor 中,这个值表示绘制时使用的颜色。这
个着色器定义了一个 uniform ,包含颜色的 4 个组件( vec4 ),保存在 uColor 中。从代码上看,这个 着色器只是把传入的值赋给了 gl_FragColor 。 uColor 的值在着色器内不能改变。
注意 OpenGL 着色器语言比示例中的代码要复杂,详细介绍需要整本书的篇幅。因此,
本节只是从使用 WebGL 的角度对这门语言做个极其简单的介绍。要了解更多信息,可以
参考 Randi J. Rost 的著作《 OpenGL 着色语言》。
创建着色器程序
浏览器并不理解原生 GLSL 代码,因此 GLSL 代码的字符串必须经过编译并链接到一个着色器程序中。为便于使用,通常可以使用带有自定义 type 属性的 <script> 元素把着色器代码包含在网页中。
如果 type 属性无效,则浏览器不会解析 <script> 的内容,但这并不妨碍读写其中的内容:
javascript
<script type="x-webgl/x-vertex-shader" id="vertexShader">
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
</script>
<script type="x-webgl/x-fragment-shader" id="fragmentShader">
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
</script>
然后可以使用 text 属性提取 <script> 元素的内容:
javascript
let vertexGlsl = document.getElementById("vertexShader").text,
fragmentGlsl = document.getElementById("fragmentShader").text;
更复杂的 WebGL 应用可以动态加载着色器。重点在于要使用着色器,必须先拿到 GLSL 代码的字
符串。
有了 GLSL 字符串,下一步是创建 shader 对象。为此,需要调用 gl.createShader() 方法,并
传入想要创建的着色器类型( gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER )。然后,调用
gl.shaderSource() 方法把 GLSL 代码应用到着色器,再调用 gl.compileShader() 编译着色器。下
面是一个例子:
javascript
let vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexGlsl);
gl.compileShader(vertexShader);
let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentGlsl);
gl.compileShader(fragmentShader);
这里的代码创建了两个着色器,并把它们保存在 vertexShader 和 fragmentShader 中。然后,
可以通过以下代码把这两个对象链接到着色器程序:
javascript
let program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
第一行代码创建了一个程序,然后 attachShader() 用于添加着色器。调用 gl.linkProgram()
将两个着色器链接到了变量 program 中。链接到程序之后,就可以通过 gl.useProgram() 方法让
WebGL 上下文使用这个程序了:
javascript
gl.useProgram(program);
调用 gl.useProgram()之后,所有后续的绘制操作都会使用这个程序。
给着色器传值
前面定义的每个着色器都需要传入一个值,才能完成工作。要给着色器传值,必须先找到要接收值
的变量。对于 uniform 变量,可以调用 gl.getUniformLocation() 方法。这个方法返回一个对象,
表示该 uniform 变量在内存中的位置。然后,可以使用这个位置来完成赋值。比如:
javascript
let uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(uColor, [0, 0, 0, 1]);
这个例子从 program 中找到 uniform 变量 uColor,然后返回了它的内存位置。第二行代码调用
gl.uniform4fv() 方法给 uColor 传入了值。
给顶点着色器传值也是类似的过程。而要获得 attribute 变量的位置,可以调用 gl.getAttrib
Location() 方法。找到变量的内存地址后,可以像下面这样给它传入值:
javascript
let aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);
这里,首先取得 aVertexPosition 的内存位置,然后使用 gl.enableVertexAttribArray()
来启用。最后一行代码创建了一个指向调用 gl.bindBuffer() 指定的缓冲区的指针,并把它保存在
aVertexPosition 中,从而可以在后面由顶点着色器使用。
调试着色器和程序
与 WebGL 中的其他操作类似,着色器操作也可能失败,而且是静默失败。如果想知道发生了什么
错误,则必须手工通过 WebGL 上下文获取关于着色器或程序的信息。
对于着色器,可以调用 gl.getShaderParameter() 方法取得编译之后的编译状态:
javascript
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader));
}
这个例子检查了 vertexShader 编译的状态。如果着色器编译成功,则调用 gl.getShaderParameter() 会返回 true 。如果返回 false ,则说明编译出错了。此时,可以使用 gl.getShaderInfoLog() 并传 入着色器取得错误。这个方法返回一个字符串消息,表示问题所在。gl.getShaderParameter() 和 gl.getShaderInfoLog()既可以用于顶点着色器,也可以用于片段着色器。
着色器程序也可能失败,因此也有类似的方法。 gl.getProgramParameter() 用于检测状态。最
常见的程序错误发生在链接阶段,为此可以使用以下代码来检查:
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
alert(gl.getProgramInfoLog(program));
}
与 gl.getShaderParameter() 一样, gl.getProgramParameter() 会在链接成功时返回 true ,
失败时返回 false 。当然也有一个 gl.getProgramInfoLog() 方法,可以在程序失败时获取错误
信息。
这些方法主要在开发时用于辅助调试。只要没有外部依赖,在产品环境中就可以放心地删除它们。
GLSL 100 升级到 GLSL 300
WebGL2 的主要变化是升级到了 GLSL 3.00 ES 着色器。这个升级暴露了很多新的着色器功能,包括 3D
纹理等在支持 OpenGL ES 3.0 的设备上都有的功能。要使用升级版的着色器,着色器代码的第一行必须是:
#version 300 es
这个升级需要一些语法的变化。
顶点 attribute 变量要使用 in 而不是 attribute 关键字声明。 576 第 18 章 动画与 Canvas 图形
使用 varying 关键字为顶点或片段着色器声明的变量,现在必须根据相应着色器的行为改为使
用 in 或 out 。
预定义的输出变量 gl_FragColor 没有了,片段着色器必须为颜色输出声明自己的 out 变量。
纹理查找函数 texture2D 和 textureCube 统一成了一个 texture 函数。
8. 绘图
WebGL 只能绘制三种形状:点、线和三角形。其他形状必须通过这三种基本形状在 3D 空间的组合
来绘制。 WebGL 绘图要使用 drawArrays() 和 drawElements() 方法,前者使用数组缓冲区,后者则
操作元素数组缓冲区。
drawArrays() 和 drawElements() 的第一个参数都表示要绘制形状的常量。下面列出了这些常量。
gl.POINTS :将每个顶点当成一个点来绘制。
gl.LINES :将数组作为一系列顶点,在这些顶点间绘制直线。每个顶点既是起点也是终点,因
此数组中的顶点必须是偶数个才能开始绘制。
gl.LINE_LOOP :将数组作为一系列顶点,在这些顶点间绘制直线。从第一个顶点到第二个顶点
绘制一条直线,再从第二个顶点到第三个顶点绘制一条直线,以此类推,直到绘制到最后一个
顶点。此时再从最后一个顶点到第一个顶点绘制一条直线。这样就可以绘制出形状的轮廓。
gl.LINE_STRIP :类似于 gl.LINE_LOOP ,区别在于不会从最后一个顶点到第一个顶点绘制直线。
gl.TRIANGLES :将数组作为一系列顶点,在这些顶点间绘制三角形。如不特殊指定,每个三角
形都分开绘制,不共享顶点。
gl.TRIANGLES_STRIP :类似于 gl.TRIANGLES ,区别在于前 3 个顶点之后的顶点会作为第三
个顶点与其前面的两个顶点构成三角形。例如,如果数组中包含顶点 A 、 B 、 C 、 D ,那么第一个
三角形使用 ABC ,第二个三角形使用 BCD 。
gl.TRIANGLES_FAN :类似于 gl.TRIANGLES ,区别在于前 3 个顶点之后的顶点会作为第三个
顶点与其前面的顶点和第一个顶点构成三角形。例如,如果数组中包含顶点 A 、 B 、 C 、 D ,那么
第一个三角形使用 ABC ,第二个三角形使用 ACD 。
以上常量可以作为 gl.drawArrays() 方法的第一个参数,第二个参数是数组缓冲区的起点索引,
第三个参数是数组缓冲区包含的顶点集合的数量。以下代码使用 gl.drawArrays() 在画布上绘制了一
个三角形:
javascript
// 假设已经使用本节前面的着色器清除了视口
// 定义 3 个顶点的 x 坐标和 y 坐标
let vertices = new Float32Array([ 0, 1, 1, -1, -1, -1 ]),
buffer = gl.createBuffer(),
vertexSetSize = 2,
vertexSetCount = vertices.length/vertexSetSize,
uColor,
aVertexPosition;
// 将数据放入缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 给片段着色器传入颜色
uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(uColor, [ 0, 0, 0, 1 ]);
// 把顶点信息传给着色器
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, vertexSetSize, gl.FLOAT, false, 0, 0);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, vertexSetCount);
这个例子定义了一个 Float32Array 变量,它包含 3 组两个点的顶点。完成计算的关键是跟踪顶
点大小和数量。将 vertexSetSize 的值指定为 2 ,再计算出 vertexSetCount 。顶点信息保存在了缓
冲区。然后把颜色信息传给片段着色器。
接着给顶点着色器传入顶点集的大小,以及表示顶点坐标数值类型的 gl.FlOAT 。第四个参数是一
个布尔值,表示坐标不是标准的。第五个参数是 步长值 ( stride value ),表示跳过多个数组元素取得下一 个值。除非真要跳过一些值,否则就向这里传入 0 即可。最后一个参数是起始偏移量,这里的 0 表示从 第一个数组元素开始。
最后一步是使用 gl.drawArrays() 把三角形绘制出来。通过把第一个参数指定为 gl.TRIANGLES ,
就可以从 (0, 1) 到 (1, --1) 再到 (--1, --1) 绘制一个三角形,并填充传给片段着色器的颜色。第二个参数表示缓 冲区的起始偏移量,最后一个参数是要读取的顶点数量。
通过改变 gl.drawArrays() 的第一个参数,可以修改绘制三角形的方式。图 18-17 展示了修改第
一个参数之后的两种输出。
9. 纹理
WebGL 纹理可以使用 DOM 中的图片。可以使用 gl.createTexture() 方法创建新的纹理,然后
再将图片绑定到这个纹理。如果图片还没有加载,则可以创建一个 Image 对象来动态加载。图片加载
完成后才能初始化纹理,因此在图片的 load 事件之后才能使用纹理。比如:
javascript
let image = new Image(),
texture;
image.src = "smile.gif"; 578 第 18 章 动画与 Canvas 图形
image.onload = function() {
texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// 除当前纹理
gl.bindTexture(gl.TEXTURE_2D, null);
}
除了使用 DOM 图片,这些步骤跟在 OpenGL 中创建纹理是一样的。最大的区别在于使用
gl.pixelStorei() 设置了像素存储格式。常量 gl.UNPACK_FLIP_Y_WEBGL 是 WebGL 独有的,在基
于 Web 加载图片时通常要使用。原因在于 GIF 、 JPEG 和 PNG 图片使用的坐标系统与 WebGL 内部的坐 标系统不一样。如果不使用这个标志,图片就会倒过来。
用于纹理的图片必须跟当前页面同源,或者是来自启用了跨源资源共享( CORS , Cross-Origin
Resource Sharing )的服务器上。
注意 纹理来源可以是图片、通过 <video> 元素加载的视频,甚至是别的 <canvas> 元素。
视频同样受跨源限制。
10. 读取像素
与 2D 上下文一样,可以从 WebGL 上下文中读取像素数据。读取像素的 readPixels() 方法与
OpenGL 中的方法有同样的参数,只不过最后一个参数必须是定型数组。像素信息是从帧缓冲区读出来 并放到这个定型数组中的。readPixels() 方法的参数包括 x 和 y 坐标、宽度、高度、图像格式、类型 和定型数组。前 4 个参数用于指定要读取像素的位置。图像格式参数几乎总是 gl.RGBA 。类型参数指的
是要存储在定型数组中的数据类型,有如下限制:
如果这个类型是 gl.UNSIGNED_BYTE ,则定型数组必须是 Uint8Array ;
如果这个类型是 gl.UNSIGNED_SHORT_5_6_5 、 gl.UNSIGNED_SHORT_4_4_4_4 或 gl.UNSIGNED_
SHORT_5_5_5_1 ,则定型数组必须是 Uint16Array 。
下面是一个调用 readPixels() 方法的例子:
javascript
let pixels = new Uint8Array(25*25);
gl.readPixels(0, 0, 25, 25, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
以上代码读取了帧缓冲区中 25 像素 ×25 像素大小的区域,并把读到的像素信息保存在 pixels 数组
中,其中每个像素的颜色在这个数组中都以 4 个值表示,分别代表红、绿、蓝和透明度值。每个数组值 的取值范围是 0~255 (包括 0 和 255 )。别忘了先按照预期存储的数据量初始化定型数组。
在浏览器绘制更新后的 WebGL 图像之前调用 readPixels() 没有问题。而在绘制完成后,帧缓冲
区会恢复到其初始清除状态,此时调用 readPixels() 会得到与清除状态一致的像素数据。如果想在绘 制之后读取像素,则必须使用前面讨论过的 preserveDrawingBuffer 选项初始化 WebGL 上下文:
javascript
let gl = drawing.getContext("webgl", { preserveDrawingBuffer: true; });
设置这个标志可以强制帧缓冲区在下一次绘制之前保持上一次绘制的状态。这个选项可能会影响性
能,因此尽量不要使用。
3 WebGL1 与 WebGL2
WebGL1 代码几乎完全与 WebGL2 兼容。在使用 WebGL2 上下文时,唯一可能涉及修改代码以保证 兼容性的就是扩展。在 WebGL2 中,很多扩展都变成了默认功能。
例如,要在 WebGL1 中使用绘制缓冲区,需要先测试相应扩展后再使用:
javascript
let ext = gl.getExtension('WEBGL_draw_buffers');
if (!ext) {
// 没有扩展的代码
} else {
ext.drawBuffersWEBGL([...])
}
而在 WebGL2 中,这里的检测代码就不需要了,因为这个扩展已经直接暴露在上下文对象上了:
gl.drawBuffers([...]);
以下特性都已成为 WebGL2 的标准特性:
ANGLE_instanced_arrays
EXT_blend_minmax
EXT_frag_depth
EXT_shader_texture_lod
OES_element_index_uint
OES_standard_derivatives
OES_texture_float
OES_texture_float_linear
OES_vertex_array_object
WEBGL_depth_texture
WEBGL_draw_buffers
Vertex shader texture access
注意 要了解 WebGL 更新的内容,可以参考 WebGL2Fundamentals 网站上的文章" WebGL2
from WebGL1 "。
四、小结
HTML5 的 <canvas> 元素为 JavaScript 提供了动态创建图形的 API 。这些图形需要使用特定上下文
绘制,主要有两种。
第一种是支持基本绘图操作的 2D 上下文:
填充和描绘颜色及图案
绘制矩形
绘制路径
绘制文本
创建渐变和图案 580 第 18 章 动画与 Canvas 图形
第二种是 3D 上下文,也就是 WebGL 。 WebGL 是浏览器对 OpenGL ES 2.0 的实现。 OpenGL ES 2.0 是游戏图形开发常用的一个标准。WebGL 支持比 2D 上下文更强大的绘图能力,包括:
用 OpenGL 着色器语言( GLSL )编写顶点和片段着色器;
支持定型数组,限定数组中包含数值的类型;
创建和操作纹理。
目前所有主流浏览器的较新版本都已经支持 <canvas> 标签。