15 使用 Canvas 绘图

本章内容

  • 理解<canvas>元素
  • 绘制简单的 2D 图形
  • 使用 WebGL 绘制 3D 图形

这个元素负责在页面中设定一个区域,然后就可以通过 JavaScript 动态地在这个区域中绘制图形。
与浏览器环境中的其他组件类似,<canvas>由几组 API 构成,但并非所有浏览器都支持。除了具备基本绘图能力的 2D 上下文,<canvas>还建议了一个名为 WebGL 的 3D 上下文。

15.1 基本用法

要使用<canvas>元素,必须先设置其widthheight属性,指定可以绘图的区域大小。出现在开始和结束标签中的内容是后备信息,如果浏览器不支持<canvas>元素,就会显示这些信息。如下例。

<canvas id="drawing" width="200" height="200">A drawing of something</canvas>

如果不添加任何样式或者不绘制任何图形,在页面中是看不到该元素的。
要在这块画布上绘图,需要取得绘图上下文。使用getContext()方法获得绘图上下文对象的引用。

var drawing = document.getElementById("drawing");
// 确定浏览器支持<canvas>元素
if (drawing.getContext) {
  var context = drawing.getContext("2d");
  //更多代码
}

使用toDataURL()方法,可以导出在<canvas>元素上绘制的图像。这个方法接受一个参数,即图像的 MIME 类型格式,而且适合用于创建图像的任何上下文。比如,要取得画布中的一幅 PNG 格式的图像,可以使用如下代码。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext) {
  //取得图像的数据 URI
  var imgURI = drawing.toDataURL("image/png");
  //显示图像
  var image = document.createElement("img");
  image.src = imgURI;
  document.body.appendChild(image);
}

默认情况下,浏览器会将图像编码为 PNG 格式。

如果绘制到画布上的图像源自不同的域,toDataURL()方法会抛出错误。

15.2 2D 上下文

使用 2D 绘图上下文提供的方法,可以绘制简单的 2D 图形,比如矩形、弧线和路径。2D 上下文的坐标开始于<canvas>元素的左下角,原点坐标是(0,0)。默认情况下,widthheight表示水平和垂直两个方向上可用的像素数目。

15.2.1 填充和描边

两种基本操作是填充和描边。大多数 2D 上下文操作都会细分为填充和描边两个操作,而操作的结果取决于两个属性:fillstylestrokeStyle
这两个属性的值可以是字符串、渐变对象或模式对象,而且它们的默认值都是“#000000”。如果为它们指定表示颜色的字符串值,可以使用 CSS 中指定颜色值得任何格式,包括颜色名、十六进制码、rgb、rgba、hsl 或 hsla。举例:

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext) {
  var context = drawing.getContext("2d");
  context.strokeStyle = "red";
  context.fillStyle = "#0000ff";
}

这样以后,所有涉及描边和填充的操作都将使用这两个样式,直至重新设置这两个值。这两个属性的值也可以是渐变对象或模式对象。

15.2.2 绘制矩形

矩形是唯一一种可以直接在 2D 上下文中绘制的形状。与矩形有关的方法包括fillRect()strokeRect()clearRect()。这三个方法都能接收 4 个参数: 矩形的 x 坐标、矩形的 y 坐标、矩形宽度和矩形高度。这些参数的单位都是像素。
首先,fillRect()方法在画布上绘制的矩形会填充指定的颜色。填充的颜色通过fillStyle属性指定。

var drawing = document.getElementById("drawing");
if (drawing.getContext) {
  var context = drawing.getContext("2d");
  //绘制红色矩形
  context.fillStyle = "#ff0000";
  cotext.fillRect(10, 10, 50, 50);
  //绘制半透明的蓝色矩形
  context.fillStyle = "rgba(0, 0, 255, 0.5)";
  context.fillRect(30, 30, 50, 50);
}

同样地,strokeRect()方法在画布上绘制的矩形会使用指定的颜色描边。

描边线条的宽度由lineWidth属性控制,该属性的值可以是任意整数。另外,通过lineCap属性可以控制线条末端的形状是平头、圆头还是方头("butt""round""square"),通过lineJoin属性可以控制线条相交的方式是圆交、斜交还是斜接("round""bevel""miter")。

最后,clearRect()方法用于清除画布上的矩形区域。本质上,这个方法可以把绘制上下文中的某一矩形区域变透明。

15.2.3 绘制路径

2D 绘制上下文支持很多在画布上绘制路径的方法。通过路径可以创造出复杂的形状和线条。要绘制路径,首先必须调用beginPath()方法,表示要开始绘制新路径。然后,再通过下列方法来实际地绘制路径。

  • arc(x, y, radius, startAngle, endAngle, counterclockwise):以(x,y)为圆心绘制一条弧线,弧线半径为radius,起始和结束角度(用弧度表示)分别为startAngleendAngle。最后一个参数表示startAngleandAngle是否按逆时针方向计算,值为false表示按顺时针方向计算。
  • arcTo(x1, y1, x2, y2, radius):从上一点开始绘制一条弧线,到(x2, y2)为止,并且以给定的半径radius穿过(x1, y1)
  • bezierCurveTo(c1x, c1y, c2x, c2y, x, y):从上一点开始绘制一条曲线,到(x, y)为止,并且以(c1x, c1y)(c2x, c2y)为控制点。
  • lineTo(x, y):从上一点开始绘制一条直线,到(x,y)为止。
  • moveTo(x,y):将绘图游标移动到(x,y),不画线。
  • quadraticCurveTo(cx, cy, x, y):从上一点开始绘制一条二次曲线,到(x, y)为止,并且以(cx, cy)作为作为控制点。
  • rect(x, y, width, height):从点(x, y)开始绘制一个矩形,宽度和高度分别由widthheight指定。这个方法绘制的是矩形路径,而不是strokeRect()fillRect()所绘制的独立的形状。

创建了路径后,接下来有几种可能的选择。如果想绘制一条连接到路径起点的线条,可以调用closePath()。如果路径已经完成,你想用fillStyle填充它,可以调用fill()方法。另外,还可以调用stroke()方法对路径描边,描边使用的是strokeStyle。最后还可以调用clip(),这个方法可以在路径上创建一个剪切区域。
看一个绘制不带数字的时钟表盘。

var drawing = document.getElementById("drawing");
//确定浏览器支持`<canvas>`元素
if (drawing.getContext) {
  var 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();
}

在 2D 绘图上下文中,路径是一种主要的绘图方式,因为路径能为要绘制的图形提供更多控制。由于路径的频繁使用,有了isPointInPath()的方法。这个方法接收 x 和 y 坐标作为参数,用于在路径被关闭之前确定画布上的某一点是否位于路径上,例如:

if (context.isPointInPath(100, 100)) {
  alert('Point (100, 100) is in the path.');
}
2D 上下文的路径 API 已经非常稳定,可以利用它们结合不同的填充和描边样式,绘制出非常复杂的图形来。

15.2.4 绘制文本

绘制文本主要有两个方法:fillText()strokeText()。这两个方法都可以接收 4 个参数:要绘制的文本字符串、x 坐标、y 坐标和可选的最大像素宽度。第四个参数存在兼容性问题。而且,这两个方法都以下列 3 个属性为基础。

  • font: 表示文本样式、大小及字体,用 CSS 中指定字体的格式来指定。
  • textAlign:表示文本对齐方式。建议使用“start”和“end”,不要使用“left”和”right“,因为前两者的意思更稳妥,能同时适合从左到右和从右到左显示(阅读)的语言。
  • textBaseline:表示文本的基线。

这几个属性都有默认值,因此没有必要每次使用它们都重新设置一遍值。fillText()方法使用fillStyle属性绘制文本,而strokeText()方法使用strokeStyle属性为文本描边。相对来说,还是使用fillText()的时候更多,因为该方法模仿了在网页中正常显示文本。例如,下面的代码在前一节创建的表盘上方绘制了数字 12:

context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);

由于绘制文本比较复杂,特别是需要把文本控制在某一区域中的时候, 2D 上下文提供了辅助确定文本大小的方法measureText()。这个方法接收一个参数,即要绘制的文本;返回一个TextMetrics对象。返回的对象目前只有一个width属性,但将来还会增加更多度量属性。
measureText()方法利用font、textAligntextBaseline的当前值计算指定文本的大小。假设你想在一个 140 像素的矩形区域中绘制文本 Hello world!,下面的代码从 100 像素的字体大小开始递减,最终会找到合适的字体大小。

var fontSize = 100;
content.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);

绘制文本还是相对比较复杂的操作,因此支持<canvas>元素的浏览器也并未完全实现所有与绘制文本相关的 API。

15.2.5 变换

通过上下文的变换,可以把处理后的图像绘制到画布上。2D 绘制上下文支持各种基本的绘制变换。创建绘制上下文时,会以默认值初始化变换矩阵,在默认的变换矩阵,所有处理都按描述直接绘制。为绘制上下文应用变换,会导致使用不同的变换矩阵应用处理,从而产生不同的结果。
可以通过如下方法来修改变换矩阵。

  • rotate(angle):围绕原点旋转图像angle弧度。
  • scale(scaleX, scaleY):缩放图像,在 x 方向乘以 scaleX,在 y 方向乘以scaleY。默认值都是 1.0。
  • traslate(x, y):将坐标原点移动到(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()。如果把前面的例子中的原点变换到表盘的中心,然后再绘制表针就容易多了。
var drawing = document.getElementById("drawing");
//确定浏览器支持`<canvas>`元素
if(drawing.getContext)  {
  var content = 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();
}

可以连续使用save()restore()方法跟踪上下文的状态变化,save()方法会使当时的设置都会进入一个栈结构,restore()方法,会在保存设置的栈结构中向前返回一级,恢复之前的状态。
需要注意的是,save()方法保存的只是对绘图上下文的设置和变换,不会保存绘图上下文的内容。

15.2.6 绘制图像

2D 绘图上下文内置了对图像的支持。如果你想把一幅图像绘制到画布上,可以使用drawImage()方法。根据期望的最终结果不同,调用这个方法时,可以使用三种不同的参数组合。最简单的调用方式是传入一个HTML<img>元素,以及绘制该图像的起点的 x 和 y 坐标。例如:

var image = document.images[0];
context.drawImage(image, 10, 10);

多传两个参数可以分别表示目标宽度和目标高度。例如:

context.drawImage(image, 50, 10, 20, 30);

还可以选择把图像中的某个区域绘制到上下文中。drawImage()方法的这种调用方式总共需要传入 9 个参数:要绘制的图像、源图像的x坐标、源图像的y坐标、源图像的宽度、源图像的高度、目标图像的x坐标、目标图像的y坐标、目标图像的宽度、目标图像的高度。
除了给drawImage()方法传入 HTML<img>元素外,还可以传入另一个<canvas>元素作为其第一个参数。这样,就可以把另一个画布内容绘制到当前画布上。

15.2.7 阴影

2D 上下文会根据以下几个属性的值,自动为形状或路径绘制出阴影。

  • shadowColor:用 CSS 颜色格式表示的阴影颜色,默认为黑色。
  • shadowOffsetX: 形状或路径x轴方向的阴影偏移量,默认为0。
    *shadowOffsetY:形状或路径y轴方向的阴影偏移量,默认为0.
  • shadowBlur:模糊的像素数,默认 0,即不模糊。

这些属性都可以通过context对象来修改。只要在绘制前为它们设置适当的值,就能自动产生阴影。例如:

var 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);

俩矩形的阴影样式相同。

15.2.8 渐变

渐变由CanvasGradient实例表示,很容易通过2D上下文你来创建和修改。要创建一个新的线性渐变,可以调用createLinearGradient()方法。这个方法接收 4 个参数:起点的x坐标、起点的y坐标、终点的x坐标、终点的y坐标。调用这个方法后,它就会创建一个指定大小的渐变,并返回CanvasGradient对象的实例。
创建了渐变对象后,下一步就是使用addColorStop()方法来指定色标。这个方法接收两个参数:色标位置和 CSS 颜色值。色标位置是一个0(开始的颜色)到1(结束的颜色)之间的数字。例如:

var gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");

此时,gradient对象表示的是一个从画布上点(30,30)到点(70,70)的渐变。起点的色标是白色,终点的色标是黑色。然后就可以把fillStylestrokeStyle设置为这个对象,从而使用渐变来绘制形状或描边:

//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);

要创建径向渐变(或放射渐变),可以使用createRadialGradient()方法。这个方法接收6个参数,对应着两个圆的圆心和半径。前三个参数指定的是起点圆的圆心和半径,后三个参数指的是终点圆的圆心及半径。
如果想从某个形状的中心点开始创建一个向外扩散的径向渐变效果,就要将两个圆定义为同心圆。

15.2.9 模式

模式其实就是重复的图像,可以用来填充或描边图形。使用createPattern()方法并传入两个参数:一个<img>元素和一个表示如何重复图像的字符串。其中,第二个参数的值与 css 的background-repeat属性值相同。例子:

var image = document.images[0];
var pattern = context.createPattern(image, "repeat");

//绘制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);

createPattern()方法的第一个参数也可以是一个<video>元素,或者另一个<canvas>元素。

15.2.10 使用图像数据

可以通过getImageData()取得原始图像数据。这个方法接收 4 个参数:要取得其数据的画面区域的 x 和 y 坐标以及该区域的像素宽度和高度。

var imageData = context.getImageData(10, 5, 50, 50);

每个ImageData对象都有三个属性:widthheightdata。其中data属性是一个数组,保存着图像中每一个像素的数据。每一个像素用 4 个元素来保存,分别表示红、绿、蓝、和透明度值。例如:

var data = imageData.data;
var red = data[0];
green = data[1];
blue = data[2];
alpha = data[3];

例如,通过修改图像数据,可以像下面这样创建一个煎蛋的灰阶过滤器。

var drawing = document.getElementById('drawing');
//确定浏览器支持<canvas>元素
if (drawing.getContext) {
  var context = drawing.getContext('2d');
  var image = document.images[0];
  var 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.data = data;
  context.putImageData(imageData, 0 ,0);
}

15.2.11 合成

还有两个会应用到 2D 上下文所有绘制操作的属性:globalAlphaglobalCompositionOperation。其中,globalAlpha是一个介于 0 和 1 之间的值(包括 0 和 1),用于指定所有绘制的透明度。默认值为 0。如果后续操作都要基于相同的透明度,就可以先把globalAlpha设置为适当值,然后绘制,最后再把它设置回默认值 0 。
第二个属性globalCompositionOperation表示后绘制的图形怎样与先绘制的图形结合。这个属性的值是字符串。

15.3 WebGL

WebGL 是针对 Canvas 的 3D 上下文。

15.3.1 类型化数组

类型化数组元素被设定为特定类型的值。核心是一个名为ArrayBuffer的类型。每个ArrayBuffer对象表示的只是内存中指定的字节数,但不会指定这些字节用于保存什么类型的数据。

  1. 视图
  2. 类型化视图

15.3.2 WebGL 上下文

目前,在支持的浏览器中,WebGL 的名字叫“experimental-webgl”

var drawing = document.getElementById('drawing');
//确定浏览器支持
if (drawing.context) {
  var gl = drawing.getContext("experimental-webgl");
  if (gl) {
    //使用 WebGl
}
}
  1. 常量
  2. 方法命名
  3. 准备绘图
  4. 视口与坐标
  5. 缓冲区
  6. 错误
  7. 着色器
  8. 编写着色器
  9. 编写着色器程序
  10. 为着色器传入值
  11. 调试着色器和程序
  12. 绘图
  13. 纹理
  14. 读取像素

15.3.3 支持

目前只适合实验性地学习

15.4 小结

HTML5 的<canvas>元素提供了一组 JavaScript API,让我们可以动态地创建图形和图像。图形是在一个特定的上下文中创建的,而上下文对象目前有两种。一种是 2D 上下文,可以执行原始的绘图操作,比如:

  • 设置填充、描边颜色和模式
  • 绘制矩形
  • 绘制路径
  • 绘制文本
  • 创建渐变和模式

第二种是 3D 上下文,即 WebGL 上下文,支持比 2D 上下文更丰富和更强大的图形图像处理能力。比如:

  • 用 GLSL 编写的顶点和片段着色器
  • 支持类型化数组,即能够将数组中的数据限定为某种特定的数值类型
  • 创建和操作纹理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,604评论 2 32
  • --绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益...
    韩七夏阅读 2,622评论 2 10
  • Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎。它提供了低...
    ShanJiJi阅读 1,456评论 0 20
  • 我想回到以前的时光 那个有你的操场 铺满了落叶的小路上 我们总喜欢用脚踩着他们咯吱的响 午后明媚的阳光 你趴过的课...
    姜小得儿阅读 145评论 0 0
  • 我一直以来都搞不懂到底什么叫真正的善良。 每个人小时候都会从父母从老师从教科书中得知,善良就是帮助那些我们应该去帮...
    日月旅者阅读 221评论 0 2