Canvas简介

Canvas API提供了一个通过JavaScript和HTML的<canvas>来绘制图形的方式,用于动画,游戏通话,数据可视化,图片编辑等方面。

Canvas API主要聚焦于2D图形,同样使用<canvas>元素的WebGL API则用于绘制硬件加速的2D和3D图形。

1 基础实例

下面这个简答的例子在画布上绘制一个绿色的长方形。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Canvas</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'green';
    ctx.fillRect(10, 10, 150, 100);
</script>
</body>
</html> 

结果如下:

 

2 基本用法

2.1 canvas元素

<canvas id="tutorial" width="150" height="150"></canvas> 

<canvas>元素看起来和<img>元素很像,唯一不同的是它没有src,alt这样的属性,实际上<canvas>标签只有两个属性,width和height。这些都是可选的,同样可以使用dom properties来设置。如不指定宽度和高度,Canvas会初始化为宽度为300像素,高度为150像素。也可以使用css来定义大小,但在绘制图像会伸缩以适应它的框架尺寸:如果css的尺寸与初始画布的比例不一致,它会出现扭曲。

注意:如果绘制出来的图像是扭曲的,尝试使用width和height属性为<canvas>明确规定宽高,而不是使用css。

id属性不是<canvas>元素特有的,而是每一个HTML元素都默认具有的属性,给<canvas>标签设置一个id属性方便在脚本中找到它。

<canvas>元素可以像任何一个普通图像一样(有margin,padding,border,background等属性)被设计。然而,这样的样式不会影响在Canvas中的实际图像,后面会讲到。当开始时没有为canvas规定样式规则,其将会完全透明。

替换内容

<canvas>元素与<img>标签的不同之处在于,就像<video>,<audio>,或者<picture>元素一样,很容易定义一些替代的内容。由于某些较老的浏览器(尤其是IE9之前版本的IE浏览器)或者文本浏览器不支持HTMLcanvas元素,在这些浏览器上应该总是能展示替代内容。

这非常简答,我们只是在<canvas>标签中提供了替换内容。不支持<canvas>的浏览器会忽略容器并在其中渲染替代内容。而支持<canvas>的浏览器会忽略在容器中包含的内容,并且只是正常渲染canvas。

举个例子,我们可以提供对canvas内容的文字描述或者动态生成内容相对应的图片,如下所示:

<canvas id="stockGraph" width="150" height="150">
    current stock price: $3.15 +0.15
</canvas>
<canvas id="clock" width="150" height="150">
    <img src="./../images/pic1.jpg" width="150" height="150" alt=""/>
</canvas> 

</canvas>标签不可省

与<img>元素不同,<canvas>元素需要结束标签</canvas>。如果结束标签不存在,则文档其余部分会被认为是替代内容,将不会显示出来。

如果不需要替代内容,一个简单的<canvas id="foo"></canvas>在所有支持canvas的浏览器中都是完全兼容的。

2.2 渲染上下文(The rendering context)

<canvas>元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。我们将注意力放在2D渲染上下文中。其它种类的上下文也提供了不同种类的渲染方式,比如,WebGL使用了基于OpenGL ES的3D上下文。

canvas期初是空白的。为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制。<canvas>元素有一个叫做getContext()的方法,这个方法是用来获得渲染上下文和它的绘画功能。getContext()有一个参数,指定上下文的格式。对于2D图像而言,如文本,可以使用CanvasRenderingContext2D。

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d'); 

代码第一行通过使用getElementById()方法为<canvas>元素得到dom对象,一旦有了元素对象,就可以使用它的getContext()方法来访问绘画上下文。

2.3 检查支持特性

替换内容是用在浏览器不支持<canvas>标签的场合。通过简单的测试getContext()方法的存在,脚本可以检查编程支持性。上面的代码可以改成下面这样:

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here

2.4 一个模板骨架

这是一个最简单的模板,我们可以把它看做之后例子的起点。

<html>
  <head>
    <title>Canvas tutorial</title>
    <script type="text/javascript">
      function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
          var ctx = canvas.getContext('2d');
        }
      }
    </script>
    <style type="text/css">
      canvas { border: 1px solid black; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="150" height="150"></canvas>
  </body>
</html> 

上面脚本中包含一个叫draw()的函数,当页面加载结束的时候会执行这个函数。通过使用在文档上加载事件来完成。只要页面加载结束,这个函数或者是类似这样的,同样可以使用window.setTimeOut(),window.setInterval()或者其他任何处理程序来调用。最后,这个例子最后生成了一个空白的canvas。

2.5 一个简单的例子

一开始,我们看个简单的例子,绘制了两个长方形,其中跟一个有着alpha透明度。我们在接下来的例子中探讨它是如何工作的。

<html>
 <head>
  <script type="application/javascript">
    function draw() {
      var canvas = document.getElementById("canvas");
      if (canvas.getContext) {
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = "rgb(200,0,0)";
        ctx.fillRect (10, 10, 55, 50);

        ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
        ctx.fillRect (30, 30, 55, 50);
      }
    }
  </script>
 </head>
 <body onload="draw();">
   <canvas id="canvas" width="150" height="150"></canvas>
 </body>
</html> 

最后运行它的结果是这样的:

3 使用canvas绘制图形

这里我们来学习如何使用canvas绘制矩形,三角形,直线,圆弧和曲线。

3.1 格栅 

在我们开始画图之前,先了解一下画布栅格以及坐标空间。上面1.2.5中的HTML模板有个宽150px,高150px的canvas元素。如下图所示,canvas元素默认被网格所覆盖。通常来说,网格的一个单元相当于canvas元素中的已像素。栅格的起点为左上角(坐标为(0, 0))。所有元素的位置都相对于远点定位。所以途中蓝色正方形左上角的坐标为距离左边(X轴)x像素,距离上边(Y轴)y像素,坐标为(x, y)。

3.2 绘制矩形

不同于SVG,<canvas>只支持两种形式的图形绘制,矩形和路径(由一系列的点连成的线段)。所有其他类型的图形都是通过一条或者多条路径组合而成的。不过我们拥有众多路径生成方法让复杂图形的绘制成为可能。

canvas提供三种方式绘制矩形:

fillRect(x, y, width, height):绘制一个填充的矩形

strokeRect(x, y, width, height):绘制一个矩形的边框

clearRect(x, y, width, height):清除指定矩形区域,让清除部分完全透明

上面的方法中都包含了相同的参数。x与y指定了在canvas画布上所绘制的矩形的左上角(相对与原点)的坐标。width和height设置矩形的尺寸。

矩形(Rectangular)例子

下面看看三个方法调用的效果

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25, 25, 100, 100);
    ctx.clearRect(45, 45, 60, 60);
    ctx.strokeRect(50, 50, 50, 50);
  }
} 

结果如下:

 

fillRect()绘制了一个边长为100px的黑色正方形。clearRect()函数从正方形的中心开始擦除一个60*60px的正方形,接着使用strokeRect()在清除的区域内生成一个50*50的正方形边框。 

接下来我们能够看到clearReact()的两个可选方法,然后我们会知道如何改变渲染图形的填充颜色及描边颜色。

不同于下面介绍的路径函数(path function),以上的三个函数绘制之后会马上显现在canvas上,即时生效。

3.3 绘制路径

图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线连成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的不住。

  1. 首先,需要创建路径起点。
  2. 然后使用画图命令去画出路径。
  3. 之后把路径封闭。
  4. 一旦路径生成,就能通过描边或者填充路径区域来渲染图形。

下面是所要用到的函数:

beginPath()
新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。

closePath()
闭合路劲过之后图形绘制命令有重新指向上下文中。

stroke()
通过线条来绘制图形轮廓。

fill()
通过填充路径的内容区域生成实心的图形。

生成路径的第一步叫做beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线,弧形,等)构成图形。而每次这个方法调用之后,里诶包清空重置,然后我们可以重新绘制新的图形。

注意:当前路径为空,即调用beginPath()之后,或者canvas刚健的时候,第一条路径构造命令通常被视为是moveTo(),无论实际上是什么。出于这个原因,你几乎总是要在设置路径之后专门指定起始位置。

第二步就是调用函数指定绘制路径。

第三,就是闭合路径closePath(),不是必须的。这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果当前点就是开始点,即图形已经闭合了,该函数什么也不做。

注意:当你调用fill()函数时,所有没有闭合的形状都会自动闭合,所以不需要调用closePath()函数,但是调用stroke()时不会自动闭合。

绘制一个三角形

例如,绘制三角形的代如下:

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                ctx.beginPath();
                ctx.moveTo(75, 50);
                ctx.lineTo(100, 75);
                ctx.lineTo(100, 25);
                ctx.fill();
            }
        } 

输出如下:

 

移动笔触

一个非常有用的函数,而这个函数实际上并不能画出任何东西,也是上面所描述的路径列表的一部分,这个函数就是moveTo()。或者可以想象一下在纸上作业,一支钢笔或者铅笔的笔尖从一个点到另一个点的移动过程。

moveTo(x, y)
将笔触移动到指定的坐标(x, y)上。

当canvas初始化或者beginPath()调用后,通常会使用moveTo()函数。通常会使用moveTo()函数设置为起点。也可以使用moveTo()绘制一些不连续的路径。看下面笑脸的例子。我们使用moveTo()方法第地方使用红线标记。

        // 绘制一个笑脸
        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');
                ctx.beginPath();

                // 画一个圆
                ctx.arc(75, 75, 50, 0, Math.PI * 2, true)
                ctx.stroke()

                // 嘴巴
                ctx.moveTo(110, 75)
                ctx.arc(75, 75, 35, 0, Math.PI, false)

                // 左眼
                ctx.moveTo(65, 65)
                ctx.arc(60, 65, 5, 0, Math.PI * 2, true)

                // 右眼
                ctx.moveTo(95, 65)
                ctx.arc(90, 65, 5, 0, Math.PI * 2, true)
                ctx.stroke()
            }
        } 

最后结果如下:

 

线

绘制直线,需要用到的方法lineTo()

lineTo(x, y)
绘制一条从当前位置到指定位置(x, y)的直线。

该方法有两个参数:x,y,代表坐标系中直线结束的点。开始点和之前绘制路径有关,之前路径结束点就是接下来的开始点。开始点也可以通过moveTo()函数改变。

下面例子绘制两个三角形,一个是填充的,一个是描边的。 

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                // 填充三角形
                ctx.beginPath();
                ctx.moveTo(25, 25);
                ctx.lineTo(105, 25);
                ctx.lineTo(25, 105);
                ctx.fill();

                // 描边三角形
                ctx.beginPath();
                ctx.moveTo(125, 125);
                ctx.lineTo(125, 45);
                ctx.lineTo(45, 125);
                ctx.closePath();
                ctx.stroke();
            }
        } 

结果从调用beginPah()函数准备绘制一个新的形状路径开始。然后使用moveTo函数移动到目标位置上。绘制结果如下:

 

填充与描边三角形步骤有所不同,上面提到,因为路径使用填充(fill)时,路径自动闭合,使用描边(stroke)则不会闭合路径。如果没有添加闭合路径closePath()到描边三角形中,则只绘制了两条线,并不是一个完整的三角形。

圆弧

绘制圆弧或者圆,使用arc方法。当然可以使用arcTo(),不过后者不是那么可靠,这里不作介绍。

arc(x, y, radius, startAngle, endAngle, anticlockwise)arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x,y)为圆心,radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认是顺时针)来生成圆轨迹。

arcTo(x1, y1, x2, y2, radius)
根据给定的控制点和边境华一段圆弧,再已直线连接两个控制点。

arc方法有6个参数,x,y为绘制的圆弧坐在圆上的圆心坐标。radius为半径。startAngle以及endAngle参数用弧度定义了开始以及结束的弧度。这些都是以x轴为基准。参数articlockwise为一个布尔值,为true时,是逆时针方向,否则顺时针方向。

注意:arc()函数中表示角的单位是弧度,不是角度。角度和弧度的js表达式:弧度=(Math.PI/180)*角度

下面的例子绘制12个不同的角度以及填充的圆弧。

下面两个for循环,生成圆弧的行列(x,y)坐标。每一段圆弧的开始都调用beginPath()。代码中,每个圆弧的参数都是可变的,实际编程中,不需要这样做。

(x,y)坐标是可变的。半径(radius)和开始角度(startAngle)都是固定的。结束角度(endAngle)在第一列开始时是180度(半圆)然后每列增加90度。最后一列形成一个完整的圆。

在第一行,第三行是顺时针的圆弧中使用clockwise语句,第二行,第四行是逆时圆弧中使用anticlockwise。if语句让第一行,第二行描边圆弧,下面两行填充路径。

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');
                for (var i = 0; i < 4; i++) {
                    for (var j = 0; j < 3; j++) {
                        ctx.beginPath();
                        var x = 25 + j * 50; // x 坐标值
                        var y = 25 + i * 50; // y 坐标值
                        var radius = 20; // 圆弧半径
                        var startAngle = 0; // 开始点
                        var endAngle = Math.PI + (Math.PI * j) / 2; // 结束点
                        var anticlockwise = i % 2 === 0; // 顺时针或逆时针

                        ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

                        if (i > 1) {
                            ctx.fill();
                        } else {
                            ctx.stroke();
                        }
                    }
                }
            }
        } 

 绘制结果如下:

 

二次贝塞尔曲线和三次贝塞尔曲线

下面例子介绍很有用的路径,贝塞尔曲线。二次以及三次贝塞尔曲线都很有用,用来绘制复杂有规律的图形。

quadraticCurveTo(cp1x, cp1y, x, y)
绘制二次贝塞尔曲线,cp1x,cp2x为一个控制点,x,y为结束点。

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。

下图能购很好的描述二者之间的关系,二次贝塞尔曲线有一个开始点(蓝色),一个结束点(蓝色),以及一个控制点(红色),而三次贝塞尔曲线有两个控制点。

参数x,y在这两个方法中都是结束点坐标。cp1x,cp1y为坐标中的第一个控制点,cp2x,cp2y为坐标中的第二个控制点。

使用二次以及三次贝塞尔曲线有一定难度,因为不同于像Adobe IIlustrators这样的矢量软件,我们所绘制的曲线没有给我们提供直接的视觉反馈。这个绘制复杂的图形变得十分困难。在下面的例子中,我们会绘制一些简单的有规律的图形。

二次贝塞尔曲线

这个例子使用多个贝塞尔曲线来渲染对话气泡。

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                // 二次贝塞尔曲线
                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();
            }
        } 

 绘制结果如下:

 

 三次贝塞尔曲线

这个例子使用三次贝塞尔曲线绘制心型。

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                //三次贝塞尔曲线
                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();
            }
        }

 绘制结果如下:

 

矩形

直接在画布上绘制矩形的三个额外方法,此外,也有rect()方法,将一个矩形路径增加到当前路径上。

rect(x, y, width, height)
绘制一个左上角坐标为(x,y),宽,高为width和height的矩形。

该方法执行的时候,moveTo()方法i自动设置坐标参数(0,0)。也就是说,当前笔触自动重置回默认坐标。 

组合使用

目前为止,每一个李子东的图形都只用到一种类型的路径,然后绘制一个图形没有显示使用数量及类型。所以在最后一个例子中,我们组合使用各种路径来重现一款著名的游戏。

function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                roundedRect(ctx, 12, 12, 150, 150, 15);
                roundedRect(ctx, 19, 19, 150, 150, 9);
                roundedRect(ctx, 53, 53, 49, 33, 10);
                roundedRect(ctx, 53, 119, 49, 16, 6);
                roundedRect(ctx, 135, 53, 49, 33, 10);
                roundedRect(ctx, 135, 119, 25, 49, 10);

                ctx.beginPath();
                ctx.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7, false);
                ctx.lineTo(31, 37);
                ctx.fill();

                for (var i = 0; i < 8; i++) {
                    ctx.fillRect(51 + i * 16, 35, 4, 4);
                }

                for (i = 0; i < 6; i++) {
                    ctx.fillRect(115, 51 + i * 16, 4, 4);
                }

                for (i = 0; i < 8; i++) {
                    ctx.fillRect(51 + i * 16, 99, 4, 4);
                }

                ctx.beginPath();
                ctx.moveTo(83, 116);
                ctx.lineTo(83, 102);
                ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
                ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
                ctx.lineTo(111, 116);
                ctx.lineTo(106.333, 111.333);
                ctx.lineTo(101.666, 116);
                ctx.lineTo(97, 111.333);
                ctx.lineTo(92.333, 116);
                ctx.lineTo(87.666, 111.333);
                ctx.lineTo(83, 116);
                ctx.fill();

                ctx.fillStyle = "white";
                ctx.beginPath();
                ctx.moveTo(91, 96);
                ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
                ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
                ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
                ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
                ctx.moveTo(103, 96);
                ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
                ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
                ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
                ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
                ctx.fill();

                ctx.fillStyle = "black";
                ctx.beginPath();
                ctx.arc(101, 102, 2, 0, Math.PI * 2, true);
                ctx.fill();

                ctx.beginPath();
                ctx.arc(89, 102, 2, 0, Math.PI * 2, true);
                ctx.fill();
            }
        }

        // 封装的一个用于绘制圆角矩形的函数.
        function roundedRect(ctx, x, y, width, height, radius) {
            ctx.beginPath();
            ctx.moveTo(x, y + radius);
            ctx.lineTo(x, y + height - radius);
            ctx.quadraticCurveTo(x, y + height, x + radius, y + height);
            ctx.lineTo(x + width - radius, y + height);
            ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
            ctx.lineTo(x + width, y + radius);
            ctx.quadraticCurveTo(x + width, y, x + width - radius, y);
            ctx.lineTo(x + radius, y);
            ctx.quadraticCurveTo(x, y, x, y + radius);
            ctx.stroke();
        } 

绘制结果如下:

 

 这里不详细讲解上面的代码,重点是绘制上下文中使用到了fillStyle属性,以及封装函数(例子中的roundedReact())。封装函数对与减少代码量以及复杂程度十分有用。后面会讲到fillStyle更多细节,这里仅仅是改变颜色,有默认的黑色到白色,然后又是黑色。

1.3.4 Path2D对象

正如前面的例子可以看到,可以使用一些列的路径和绘画命令来把对象"画"在画布上。为了简化代码和提高性能,Path2D对象已经可以在新版本的浏览器中使用,用来缓存或记录绘画命令,这样能快速地回顾路径。

如何产生一个Path2D对象呢?

Path2D()
Path2d()会返回一个初始化的Path2D对象(可能将某一个路径作为变量-创建一个它的副本,或者将一个包含SVG path数据的字符串作为变量)。

        new Path2D();     // 空的Path对象
        new Path2D(path); // 克隆Path对象
        new Path2D(d);    // 从SVG建立Path对象      

所有路径方法比如moveTo,reac,arc或者quadraticCurveTo等,如我们前面见过的,都可以在Path2D中使用。

Path2D API添加了addPath作为将path结合起来的方法。当你想从几个元素中来创建对象时,这将很实用,比如:

Path2D.addPath(path [, transform])
添加一条路径到当前路径(可能添加了一个变换矩阵)

Path2D示例

在这个例子中,我们创造了一个矩形和一个圆,他们都被村委Path2D对象,后面再排上用场。随着新的Path2D API产生,几种方法也相应地被更新来使用Path2D对象而不是当前路径。在这里,待参数的stroke和fill可以把对象画在画布上。

        function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas.getContext) {
                var ctx = canvas.getContext('2d');

                var rectangle = new Path2D();
                rectangle.rect(10, 10, 50, 50);

                var circle = new Path2D();
                circle.moveTo(125, 35);
                circle.arc(100, 35, 25, 0, 2 * Math.PI);

                ctx.stroke(rectangle);
                ctx.fill(circle);
            }
        } 

绘制结果如下:

 

使用SVG Paths

新的Path2D API有另一个强大的特点,就是使用SVG path data来初始化canvas上的路径。这样在获取路径时可以以SVG或者canvas的方式来重用他们。

这条路径将先移动到(M10 10)然后再水平移动80个单位(h 80),然后下移80个单位(v 80),接着左移80个单位(h -80),再回起点(Z)。

var p = new Path2D("M10 10 h 80 v 80 h -80 Z");

4 使用样式和颜色

上面只用到默认的线条和填充样式,这里探讨canvas的全部可选项,来绘制更加吸引人的内容。

4.1 颜色Colors

到目前位置,我们只看到过绘制内容的方法。如果要想给图形上色,有两个重要的属性可以做到:fillStyle,strokeStyle。

fillStyle = color::设置图形的填充颜色
strokeStyle = color:设置图形轮廓的颜色

color可以是css颜色值字符串,渐变对象或者是图案对象。默认情况下线条和填充颜色都是黑色的(css值是#000000)

注意:一旦设置了strokeStyle或者fillStyle的值,那么这个新值就会成为新绘制的图形的默认值。如果想要给每个图形上不同的颜色,需要重新设置fillStyle或strokeStyle的值。

你输入的值应该是符合css3标准的有效字符串,下面的例子都表示同一种颜色。

// 这些 fillStyle 的值均为 '橙色'
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)"; 

fillStyle示例

在本示例中,我们会再度用两层for循环来绘制方格阵列,每个方格不同的颜色。结果如下图,但是实现所用的代码没有那么复杂。使用两个变量i,j来为每个方格产生唯一的RGB色彩值,其中仅修改红色和绿色通道的值,而保持蓝色通道的值不变。可以通过修改这些颜色通道的值来产生各种各样的颜色板。通过增加渐变的频率,还可以绘制出类似Photoshop中的调色板。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  for (var i=0;i<6;i++){
    for (var j=0;j<6;j++){
      ctx.fillStyle = 'rgb(' + Math.floor(255-42.5*i) + ',' + 
                       Math.floor(255-42.5*j) + ',0)';
      ctx.fillRect(j*25,i*25,25,25);
    }
  }
} 

结果如下:
 

strokeStyle示例

这个示例与上面的有点类似,单这里用到的是strokeStyle属性,画的不是方格,而是用arc来画圆。

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    for (var i=0;i<6;i++){
      for (var j=0;j<6;j++){
        ctx.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' + 
                         Math.floor(255-42.5*j) + ')';
        ctx.beginPath();
        ctx.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
        ctx.stroke();
      }
    }
  } 

结果如下:

 

4.2 透明度

除了绘制实色图形,还可以用canvas来绘制半透明的图形。通过设置globalAlpha属性或者使用一个半透明的颜色作为轮廓或填充的样式。

globalAlpha = transparencyValue:这个属性影响到canvas中所有图形的透明度,有效的值范围是0.0(完全透明)到1.0(全完不透明),默认值是1.0.

globalAlpha属性在需要绘制大量拥有相同透明度的图形的时候相当高效。不过我认为下面的方法可操作性更强一些。

因为strokeStyle和fillStyle属性接受符合css3规范的颜色值,那么我们可用rgba对象来设置具有透明度的颜色。

// 指定透明颜色,用于描边和填充样式
ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)"; 

rgba()方法与rgb()方法类似,就多了一个用于设置色彩透明度的参数。它的有效范围是从0.0(完全透明)到1.0(完全不透明)。

globalAlpha实例

在这个例子里,用四色格作为北京,设置globalAlpha为0.2后,在上面画一系列半径递增的透明圆。最终结果是一个径向渐变的效果。圆叠加得越多,原先所画的透明度会约低。通过增加循环次数,画更多的圆,从中心到边缘部分,背景会呈现逐渐消失的效果。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  // 画背景
  ctx.fillStyle = '#FD0';
  ctx.fillRect(0,0,75,75);
  ctx.fillStyle = '#6C0';
  ctx.fillRect(75,0,75,75);
  ctx.fillStyle = '#09F';
  ctx.fillRect(0,75,75,75);
  ctx.fillStyle = '#F30';
  ctx.fillRect(75,75,75,75);
  ctx.fillStyle = '#FFF';

  // 设置透明度值
  ctx.globalAlpha = 0.2;

  // 画半透明圆
  for (var i=0;i<7;i++){
      ctx.beginPath();
      ctx.arc(75,75,10+10*i,0,Math.PI*2,true);
      ctx.fill();
  }
} 

结果如下:

 

rgba()实例

下面这个例子和上面类似,不过不是画圆,而是画矩形。这里还可以看出,rgba()可以分别设置轮廓和填充样式,因而具有更好的可操作性和灵活性。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');

  // 画背景
  ctx.fillStyle = 'rgb(255,221,0)';
  ctx.fillRect(0,0,150,37.5);
  ctx.fillStyle = 'rgb(102,204,0)';
  ctx.fillRect(0,37.5,150,37.5);
  ctx.fillStyle = 'rgb(0,153,255)';
  ctx.fillRect(0,75,150,37.5);
  ctx.fillStyle = 'rgb(255,51,0)';
  ctx.fillRect(0,112.5,150,37.5);

  // 画半透明矩形
  for (var i=0;i<10;i++){
    ctx.fillStyle = 'rgba(255,255,255,'+(i+1)/10+')';
    for (var j=0;j<4;j++){
      ctx.fillRect(5+i*14,5+j*37.5,14,27.5)
    }
  }
} 

结果如下:

 

1.4.2 线型 Line styles

可以通过一系列的属性来设置线的样式。

lineWidth = value:设置线条的宽度
lineCap = type:设置线条末端样式
lineJoin = type:设置线条与线条间接合处的样式
miterLimit = value:显示当两条线相交时交接处最大的长度,所谓交接处长度(斜接长度)是指线条交界处内角顶点到外角顶点的长度
getLineDash():返回一个包含当前虚线样式,长度为非负偶数的数组
lineDashOffset = value:设置虚线样式的起始偏移量

通过下面的例子可能会更加容易的理解。

lineWidth属性的例子

这个属性设置当前绘线的粗细。属性值必须为正数,默认值是1.0。

线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。因为画布的坐标并不和像素直接对应,当需要获得精确的水平或者垂直线的时候要特别注意。

在下面的例子中,用递增的宽度绘制了10条直线。最左边的线宽1.0单位。并且最左边的以及所有宽度为奇数线并不能精确呈现,这就是因为路径定位的问题。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        for (var i = 0; i < 10; i++) {
            ctx.lineWidth = 1 + i;
            ctx.beginPath();
            ctx.moveTo(5 + i * 14, 5);
            ctx.lineTo(5 + i * 14, 140);
            ctx.stroke();
        }
    } 

要想获得精确的线条,必须对线条是如何描绘出来的有所理解。见下图,用网格来代表canvas的坐标格,每一格对应屏幕上的一个像素点。在第一个图中,填充了(2,1)至(5,5)的矩形,整个区域的边界刚好落在像素的边缘上,这样可以得到的矩形有清晰的边缘。

 

如果想绘制一条从(3,1)到(3,5),宽度是1.0的线条,会得到像第二幅图一样的结果。实际填充区域(深蓝色部分)仅仅延伸至路径两旁各一半像素。而这半个像素又会以近似的方式进行渲染,这意味这那些像素只是部分着色,结果是以实际笔触颜色一半色调的颜色来填充整个区域(浅蓝色和深蓝的部分)。这就是上例中为何宽度为1.0的线并不准确的原因。

要解决这个问题,必须对路径控制得更加精确。1.0的线条会在路径两边延伸半像素,那么第三幅那样绘制从(3.5,1)到(3.5,5)的线条,其边缘正好落在像素边界,填充出来就是准确的1.0的线条。

注意:在这个竖线的例子中,其Y坐标刚好落在网格线上,否则断点上同样会出现半渲染的像素点(这种行为取决于当前的lineCap风格,它默认为butt;可以通过lineCap样式设置为square正方形,来得到与技术宽度线的半像素坐标一致的笔画,这样端点轮廓外边框将自动扩展以完全覆盖整个像素格)。

只有路径的起点,终点受词影响:如果一个路径是通过closePath()来封闭的,它没有起点和终点;相反的情况下,路径上的所有端点都与上一个点相连,下一段路径使用点钱的lineJoin设置(默认为miter),如果路径是水平或垂直的话,会导致相连路径的外轮廓根据焦点自动延伸,因此渲染出的路径轮廓会覆盖整个像素格。

对于那些宽度为偶数的线条,每一边的像素都是整数,那么想要其路径是落在像素点之间(如从(3,1)到(3,5))而不是在像素点的中间。同样,注意到那个例子的垂直线条,其Y坐标刚好落在网格线上,如果不是的话,断点上同样会出现半渲染的像素点。

虽然开始处理可缩放的2D图形时会很痛苦,的那是及早注意到像素网格与路径位置之间的关系,可以确保图形在经过缩放或者是其他任何变形后都可以保持看上去蛮好:线宽为1.0的垂线在放大2倍后,会变成线宽为2.0,并且出现在它应该出现的位置上。

lineCap属性的例子

属性lineCap的值决定了线段断点显示的样子。它可以为下面三种的之一:butt,round和square。默认是butt。

在这个例子中,绘制了三条直线,分别赋予不同的lineCap值。还有两条辅助线,为了可以看得更清除他们之间的区别,三条线的起点重点都落在辅助线上。

最左边的线使默认的butt。可以注意到它与辅助线齐平。中间是round的效果,断点处加上了半径为一半线宽的半圆。右边是square的效果,端点处加上了等宽且高度为一半线宽的方块。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var lineCap = ['butt', 'round', 'square'];

        // 创建路径
        ctx.strokeStyle = '#09f';
        ctx.beginPath();
        ctx.moveTo(10, 10);
        ctx.lineTo(140, 10);
        ctx.moveTo(10, 140);
        ctx.lineTo(140, 140);
        ctx.stroke();

        // 画线条
        ctx.strokeStyle = 'black';
        for (var i = 0; i < lineCap.length; i++) {
            ctx.lineWidth = 15;
            ctx.lineCap = lineCap[i];
            ctx.beginPath();
            ctx.moveTo(25 + i * 50, 10);
            ctx.lineTo(25 + i * 50, 140);
            ctx.stroke();
        }
    } 

结果如下:

  

linJoin属性的例子

lineJoin的属性值决定了图形中两线端连接处所显示的样式。它可以是这三种之一:round,bevel和miter。默认是miter。这里用三条折线来做例子,分别设置不同的lineJoin值。最上面一条是round的效果,外边角处被磨圆了,圆的半径等于线宽。中间和最下面一条分别是bevel和miter的效果。当值是miter的时候,线段会在连接处外侧延伸直至相交玉于一点,延伸效果受下面要介绍的miterLimit属性的制约。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var lineJoin = ['round', 'bevel', 'miter'];
        ctx.lineWidth = 10;
        for (var i = 0; i < lineJoin.length; i++) {
            ctx.lineJoin = lineJoin[i];
            ctx.beginPath();
            ctx.moveTo(-5, 5 + i * 40);
            ctx.lineTo(35, 45 + i * 40);
            ctx.lineTo(75, 5 + i * 40);
            ctx.lineTo(115, 45 + i * 40);
            ctx.lineTo(155, 5 + i * 40);
            ctx.stroke();
        }
    } 

结果如下:

 

miterLimit属性的例子

就如上一个例子中的miter效果,线段外侧边缘会延伸交汇于一点上。线段直接夹角比较大的,交点不会太远,但当夹角减少时,交点距离会呈指数级增大。miterLimit属性就是用来设定外延交点与连接点的最大距离,如果交点距离大于此值,连接效果变成了bevel。

手动改变miterLimit的值,观察其影响效果。蓝色辅助线显示锯齿折线段的起点与终点所在的位置。

 

使用虚线

使用setLineDash方法和lineDashOffset属性来指定虚线样式,setLineDash方法接受一个数组,来指定线段与间隙的交替;lineDashOffset属性设置其实偏移量。它往往应用在计算机图形程序选区工具动效中。它可以帮助用户通过动画的边界来区分图像背景选区边框。在本教程的后面部分,你可以学习如何实现这一点和其他基本的动画。

在下面这个例子中,我们要创建一个蚂蚁线的效果。

    var ctx = document.getElementById('canvas').getContext('2d');
    var offset = 0;

    function draw() {
        ctx.clearRect(0,0, canvas.width, canvas.height);
        ctx.setLineDash([4, 2]);
        ctx.lineDashOffset = -offset;
        ctx.strokeRect(10,10, 100, 100);
    }

    function march() {
        offset++;
        if (offset > 16) {
            offset = 0;
        }
        draw();
        setTimeout(march, 20);
    }
    march(); 

结果如下:

4.3 渐变Gradients

就好像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或者描边。我们用下面的方法新建一个canvasGradient对象,并且赋值给图形的fillStyle和strokeStyle属性。

createLinearGradient(x1, y1, x2, y2):createLinearGradient方法接受4个参数,表示渐变的起点(x1, y1)与终点(x2, y2)。
createRadiaGradient(x1, y1, r1, x2, y2, r2):createRadialGradient方法接受6个参数,前三个定义一个以(x1, y1)为原点,半径为r1的圆,后三个参数则定义一个以(x2, y2)为原点半径为r2的圆。

var lineargradient = ctx.createLinearGradient(0,0,150,150);
var radialgradient = ctx.createRadialGradient(75,75,0,75,75,100); 

创建出canvasGradient对象后,我们就可以用addColorStop方法给它上色了。

gradient.addColorStop(position, color):addColorStop方法接受2个参数,position参数必须是一个0.0至1.0之间的数值,表示渐变中颜色所在的相对位置。例如,0.5表示颜色会出现在正中间。color参数必须是一个有效的css颜色值,例如(#FFF,rgba(0, 0, 0, 1))等等。

可以根据需要it安家任意多个色标(color stops)。下面是最简单的线性黑白渐变的例子。

    var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
    lineargradient.addColorStop(0, 'white');
    lineargradient.addColorStop(1, 'black'); 

createLinearGradient的例子

这里,设置两种不同的渐变,第一种是背景色渐变,可以给同一位置设置两种颜色,可以用这个实现突变的效果,就像这里从白色到绿色的突变。一般情况下,色标的顺序是无所谓顺序的,但是色标位置重复时,顺序就变得非常重要了。所以注意保持色标定义顺序和它理想的顺序一致。

第二种渐变,并不是从0.0位置开始定义色标,因为那并不是那么严格的。在0.5处设一黑色色标,渐变会默认为从起点到色标之间都是黑色。

你会发现,strokeStyle和fillStyle属性都可以接受canvasGradient对象。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        // Create gradients
        var lingrad = ctx.createLinearGradient(0, 0, 0, 150);
        lingrad.addColorStop(0, '#00ABEB');
        lingrad.addColorStop(0.5, '#fff');
        lingrad.addColorStop(0.5, '#26C000');
        lingrad.addColorStop(1, '#fff');
        var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95);
        lingrad2.addColorStop(0.5, '#000');
        lingrad2.addColorStop(1, 'rgba(0,0,0,0)');
        // assign gradients to fill and stroke styles
        ctx.fillStyle = lingrad;
        ctx.strokeStyle = lingrad2;
        // draw shapes
        ctx.fillRect(10, 10, 130, 130);
        ctx.strokeRect(50, 50, 50, 50);
    } 

结果如下:

createRadialGradient的例子

在这个例子中定义4个不同的径向渐变。可以控制渐变的起始和结束点,所以我们可以是实现一些更加复杂的效果。(经典的径向渐变是只有一个中心店,简单地由中心向外围的圆扩张)

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        // 创建渐变
        var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
        radgrad.addColorStop(0, '#A7D30C');
        radgrad.addColorStop(0.9, '#019F62');
        radgrad.addColorStop(1, 'rgba(1,159,98,0)');

        var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50);
        radgrad2.addColorStop(0, '#FF5F98');
        radgrad2.addColorStop(0.75, '#FF0188');
        radgrad2.addColorStop(1, 'rgba(255,1,136,0)');

        var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40);
        radgrad3.addColorStop(0, '#00C9FF');
        radgrad3.addColorStop(0.8, '#00B5E2');
        radgrad3.addColorStop(1, 'rgba(0,201,255,0)');

        var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90);
        radgrad4.addColorStop(0, '#F4F201');
        radgrad4.addColorStop(0.8, '#E4C700');
        radgrad4.addColorStop(1, 'rgba(228,199,0,0)');

        // 画图形
        ctx.fillStyle = radgrad4;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad3;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad2;
        ctx.fillRect(0, 0, 150, 150);
        ctx.fillStyle = radgrad;
        ctx.fillRect(0, 0, 150, 150);
    } 

结果如下:

  

这里让起点稍微偏离终点,这样可以达到一种球状3D效果。但最好不要让里面的圆与外圆部分交叠,那样会产生什么样的效果就不好说了。4个径向渐变效果的最后一个色标是透明的,如果想要两色标直接的过渡柔和一些,只要两个颜色值一致就可以了。代码里看不出来,是因为用了两种不同的颜色表示方法,其实是相同的,#019F62=rgba(1,159,98,1)。

4.4 图案样式Patterns

上面我们使用循环来实现图案的效果,其实有一个更加简单方式:createPattern。

createPattern(image, type):该方法接受两个参数。Image可以是一个Image对象的引用,或者另一个canvas对象。Type必须是下面的字符串之一:repeat,repeat-x,repeat-y和norepeat。

注意:canvas对象作为Image参数在FireFox1.5(Gecko 1.8)中是无效的。

图案的应用跟渐变很类似,创建一个pattern之后,赋给fillStyle或strokeStyle属性即可。

var img = new Image();
img.src = 'someimage.png';
var ptrn = ctx.createPattern(img,'repeat');

注意:与drawImage有点不同,你需要确认image对象已经装载完毕,否则图案可能效果不对。

createPattern的例子

在最后的例子中,床架一个图案然后赋给了fillStyle属性。唯一要注意的是,使用Image对象的onload handle来确保设置图案之间图像已经装载完毕。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        // 创建新 image 对象,用作图案
        var img = new Image();
        img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
        img.onload = function () {
            // 创建图案
            var ptrn = ctx.createPattern(img, 'repeat');
            ctx.fillStyle = ptrn;
            ctx.fillRect(0, 0, 150, 150);
        }
    } 

结果如下:

 

4.5 阴影Shadows

shadowOffsetX=float,shadowOffsetY=float:shadowOffsetX和shadowOffsetY用来设定阴影在X轴和Y轴的延伸距离,他们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值表示会往下或右延伸,他们默认值都为0。
shadowBlur=float:shadowBlur用于设定阴影的模糊程度,其数值并不跟相熟数量挂钩,也不受变换矩阵的影响,默认为0。
shadowColor=color:shadowColor是标准的CSS颜色值,用于设定阴影颜色效果,默认是全透明的黑色。

文字阴影的例子

这个例子绘制了带阴影效果的文字。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowBlur = 2;
        ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

        ctx.font = "20px Times New Roman";
        ctx.fillStyle = "Black";
        ctx.fillText("Sample String", 5, 30);
    } 

结果如下:

 

4.6 canvas填充规则

当我们用到fill(或者clip和isPointinPath)可以选择一个填充规则,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己与自己路径相交或者路径被嵌套的时候非常有用。

两个可能的值:

  • “nonzero”:non-zero winding rule,默认值
  • “evenodd”:even-odd winding rule

这个例子,我们用填充规则evenodd

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.beginPath();
        ctx.arc(50, 50, 30, 0, Math.PI * 2, true);
        ctx.arc(50, 50, 15, 0, Math.PI * 2, true);
        ctx.fill("evenodd");
    } 

结果如下:

 

5. 绘制文本

5.1 绘制文本

canvas提供两种方式来绘制文本:

filltext(text, x, y, [, maxWidth]):在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的
strokeText(text, x, y, [, maxWidth]):在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的

一个填充文本是例子

文本用当前填充方式被填充

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.font = "48px serif";
        ctx.fillText("Hello world", 10, 50);
    } 

结果如下:

 

 一个文本边框的例子

文本用当前的边框样式被绘制

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.font = "48px serif";
  ctx.strokeText("Hello world", 10, 50);
} 

结果如下:

 

5.2 有样式的文本

上面使用font属性来使文本比默认尺寸大一些,还有更多的属性来改变canvas文本的样式。

font=value:用来绘制文本的样式,这个字符串和css font属性相同。默认的字体是10px sans-serif
textAlign=value:文本对其选项,可选的值有start,end,left,right,center。默认值是start
textBaseline=value:基线对齐选项,可选的值包括:top,hanging,middle,alphabetic,ideographic,bottom。默认的值是alphabetic。
direction=value:文本方向,可能的值:ltr,rtl,inherit。默认值inherit。

如果使用过css,这些属性应该很熟悉了。

textBaseline例子

    function draw() {
        var ctx = document.getElementById("canvas").getContext('2d')
        ctx.font = "48px serif"
        ctx.textBaseline = "hanging"
        ctx.strokeText("hello world", 0, 100)
    } 

 

 5.3 预测文本宽度

当需要获得更多文本细节时,可以使用下面方法测量文本。

measureText():方法返回一个TextMetrics对象的宽度,所在像素,这些体现文本特性的属性。

下面这段代码展示如何测量文本来获取它的宽度:

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  var text = ctx.measureText("foo"); // TextMetrics object
  text.width; // 16;

6. 使用图像

canvas更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的格式的任意的外部图片都可以使用,比如PNG,GIF,JPEG等。甚至可以将同一个页面中其他canvas元素生成的图片作为图片源。

引入图片都canvas需要两个基本步骤:

  1. 获得一个指向HtmlImageElement对象或者另一个canvas元素的引用作为源,也可以通过提供一个URL的方式来使用图片。
  2. 使用drawImage()函数将图片绘制到画布上。

6.1 获得需要绘制的图片

canvas的API可以使用下面这些类型中的一种作为图片的源:

HTMLImageElement:这些图片是由Image()函数构造出来的,或者任何的<img>元素。
HTMLVideoElement:用一个HTML的<video>元素作为图片原,可以从适配中抓取当前帧作为一个图像。
HTMLCanvasElement:可以使用另一个<canvas>元素作为图片源。
ImageBitmap:这是一个高性能的位图,可以低延迟地绘制,他可以从上述所有源以及其他几种源中生成。

这些源统一由CanvasImageSource类型来引用。有几种方式可以获取到我们需要在canvas上使用的图片。

使用相同页面内的图片

我们可以使用下面方法中的一种获取与canvas同一个页面中的图片的引用:

  • document.images集合
  • document.getElementByTagName()方法
  • 如果知道指定图片的ID,可以使用document.getElementById()方法获取这个图片

使用其他域名下的图片

在HTMLImageElement上使用crossOrigin属性,可以请求加载其他域名上的图片。如果图片的服务器允许跨域访问这个图片,那么可以使用这个图片而不污染canvas,否则这个图片将会污染canvas。

使用其他canvas元素

和引用页面内的图片类似,用document.getElementByTagName或者document.getElementById方法来获取其他canvas元素,但是引入的应该是已经准备好的canvas。

一个常用的应用就是将第二个canvas作为另一个更大的canvas的缩略图。

由零开始创建图像

或者我们可以是用脚本创建一个新的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'; // 设置图片源地址 

如果只用到一张图片的话,这已经足够了,但是一旦需要不止一张图片,情况就变得复杂了,当时图片预加载的策略不在这里讨论。

通过data:url方式嵌入图像

我们还可以通过data:url的方式来引用图像,Data urls允许使用一串Base64编码的字符串的方式来定义一个图片,如下:

img.src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw=='; 

其优点是图片内容可以直接使用,无须再到服务器兜一圈。(还有一个优点是,可以将css,JavaScript,html和图片全部封装在一起,前一起来比较方便)缺点是没办法缓存图片,大图片,高清图片的话内嵌url数据会非常长。

使用视频帧

还可以使用<video>中的帧,即使是不可见的video。例如,如果有一个id为myvideo的<video>元素,可以这样做:

function getMyVideo() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    return document.getElementById('myvideo');
  }
} 

它将为这个视频返回HTMLVideoElement对象,正如我们前面说到的,可以使用它作为canvas图片源。

6.2 绘制图片

一旦获得了源图片对象,我们可以使用drawImage来将图像渲染到canvas里。drawImage有三种重载,下面是最基础的一种。

drawImage(image, x, y):其中image是image或者canvas对象,x, y是其在目标canvas中的起始坐标。

一个简单的例子

下面是一个例子,我们用一个外部的图像作为一个折线图的背景图。这样我们就不必绘制复杂的背景图了,省下不少代码。这里只用到一个image对象,于是就在它的onload事件响应函数中绘制折线图。drawImage方法将背景放置在canvas的左上角(0,0)处。

function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    var img = new Image();
    img.onload = function(){
      ctx.drawImage(img,0,0);
      ctx.beginPath();
      ctx.moveTo(30,96);
      ctx.lineTo(70,66);
      ctx.lineTo(103,76);
      ctx.lineTo(170,15);
      ctx.stroke();
    }
    img.src = 'images/backdrop.png';
  } 

看起来结果是这样的:

 

6.2 缩放Scaling

drawImage方法的了一种重载变种是增加了两个用于控制图像在canvas中缩放的参数。

drawImage(image, x, y, width, height):这个方法多了2个参数,width和height,这两个参数用来控制当向canvas画入时应该缩放的大小。

平铺图像

在这个例子中,用一张图片,像背景一样在canvas中重复平铺。实现起来也很简单,只要循环铺开经过缩放的图片即可。如下面代码,第一层for循环做行重复,第二层是做列重复的。图像大小被缩放到原来的三分之一,50x38px。这种方法很好达到背景图案的效果,在下面的例子中可以看到。

注意:图像很可能因为大幅度的缩放而变得模糊,如果图像中有文字,那么最好还是不要缩放,因为那样处理后很可能因为图片模糊,使图像中的文字无法辨认。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        var img = new Image();
        img.onload = function () {
            for (var i = 0; i < 4; i++) {
                for (var j = 0; j < 3; j++) {
                    ctx.drawImage(img, j * 50, i * 38, 50, 38);
                }
            }
        };
        img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
    }

所谓缩放就是设置图片的大小,这样这样图片的宽和高可能和原始的宽,高比例不同,这样图片会出现拉伸,伸缩效果。这个例子就是从固定位置,按照固定大小,在canvas上绘制图像。结果如下:

6.3 切片Slicing

drawImage方法第三个重载有8个参数,用于控制做切片显示的。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):第一个参数是图像源,和上面的是相同的,都是一个图像或另一个canvas的应用。其他的参数参照下图理解,前4个参数是定义图像源的切片位置和大小,后4个参数是定于切片的目标显示位置和大小。

 

切片是做图像合成的强大工具。假设有一张图像,包含了所有图片元素(比如,一张图片上有,前进按钮,后退按钮,主页按钮),可以用这种方式来合成完整的图像。例如想画一张图标,而手头只有包含所有必需文字的PNG文件,那么可以轻松的根据实际数据的需要来改变最终显示的图标。这方法的另一个好处是你不需要单独装载每一个图像。

一个相框的例子

在这个例子中,继续使用上面的犀牛图片,不过通过切片将它放在一个相框图片中。相框图片是一个24位的PNG图片,中间有一个透明的阴影。因为24位PNG图像包含一个完整的8位alpha通道,不像GIF和8未的PNG图像,它可以放在任何背景上,而不用担心遮罩。

 

下面代码思路是在画布上先用犀牛图像作为源画一次,再用相框作为源画一次。

注意这里没有通过创建新对象来加载图像,而是将他们作为<img>标签直接包含在HTML源代码中,并从中检索图像。通过display隐藏犀牛和相框。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas-image</title>
</head>
<body>
<canvas id="canvas" width="150" height="150"></canvas>
<div style="display:none;">
    <img id="source" src="https://mdn.mozillademos.org/files/5397/rhino.jpg" width="300" height="227">
    <img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150">
</div>
<script>
    function draw() {
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');

        // Draw slice
        ctx.drawImage(document.getElementById('source'), 33, 71, 104, 124, 21, 20, 87, 104);

        // Draw frame
        ctx.drawImage(document.getElementById('frame'), 0, 0);
    }
    draw();
</script>
</body>
</html> 

结果如下:

  

美术馆的例子

通过canvas切片,既然可以把犀牛放在相框里,那多个头像放在相框中就是一个美术馆的效果。在这个例子中,将建立一个小型的美术馆。画廊由多个图像的表格组成。加载页面时canvas将为每个图像插入一个元素,并在其周围绘制一个框架。

每个图像有固定的宽度和高度,周围绘制的框架也是如此。可以增强脚本,使其使用图像的宽度和高度来调整框架摆放位置。

代码的逻辑如下,遍历document.images容器并响应的添加新的canvas元素。唯一需要注意的是使用Node.insertBefore方法。insertBefore是元素(图像)的父节点(表单元)的方法,在此之前我们要插入新节点(canvas元素)。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>album</title>
    <style>
        body {
            background: 0 -100px repeat-x url(https://mdn.mozillademos.org/files/5415/bg_gallery.png) #4F191A;
            margin: 10px;
        }
        img { display: none; }
        table { margin: 0 auto; }
        td { padding: 15px; }
    </style>
</head>
<body onload="draw();">
<table>
    <tr>
        <td><img src="https://mdn.mozillademos.org/files/5399/gallery_1.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5401/gallery_2.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5403/gallery_3.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5405/gallery_4.jpg"></td>
    </tr>
    <tr>
        <td><img src="https://mdn.mozillademos.org/files/5407/gallery_5.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5409/gallery_6.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5411/gallery_7.jpg"></td>
        <td><img src="https://mdn.mozillademos.org/files/5413/gallery_8.jpg"></td>
    </tr>
</table>
<img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150"/>
<script>
    function draw() {
        // 遍历页面中的image
        for (var i = 0; i < document.images.length; i++) {
            // 相框就算了
            if (document.images[i].getAttribute('id') != 'frame') {
                // 创建一个canvas
                canvas = document.createElement('canvas');
                canvas.setAttribute('width', 132);
                canvas.setAttribute('height', 150);
                // 在插入在每个隐藏的图片前面
                document.images[i].parentNode.insertBefore(canvas,document.images[i]);
                ctx = canvas.getContext('2d');
                // 在canvas中画这个图片
                ctx.drawImage(document.images[i], 15, 20);
                // 在canvas中画这个像框
                ctx.drawImage(document.getElementById('frame'), 0, 0);
            }
        }
    }
</script>
</body>
</html> 

效果如下:

  

 6.4 控制图像的缩放行为

如前所述,缩放图像可能会由于缩放过程而导致图片模糊,变形。在上下文context中imageSmoothingEnabled属性来控制缩放图像时是否平滑,默认值是true,图片被裁剪时保持平滑。设置方式如下:

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false

7. 变形Transformations

前面的内容中讨论了canvas网格和坐标空间。目前为止,我们只根据需要改变默认网格的大小。通过使用变形这种强大的方式可以将图形源旋转,缩放。

7.1 状态的保存和恢复

在深入变形之前,先了解两个方法,这两个方法是实现复杂绘图必不可少的。

save():保存canvas的所有状态
restore():从最近使用的场景中恢复canvas的状态

每一次调用save()方法的时候,canvas状态就会保存在一个栈中,当前绘图状态保被推入栈顶。一个绘图状态由下面内容组成:

  • 当前应用的变形(例如,移动,旋转,伸缩)
  • 以下一些属性的值:strokeStyle,fillStyle,globalAlpha,lineWidth,lineCap,lineJoin,miterLimit,lineDashOffset,shadowOffsetX,shadowOffsetY,shadowBlur,shadowColor,globalCompositeOperation,font,textAlign,textBaseline,direction,imageSmoothingEnabled
  • 当前裁剪路径,下面将会介绍

可以任意次数的调用save()方法。每次调用restore()方法最后一次保存的绘图状态从栈顶弹出,左右状态被重新装载为次栈顶的状态。

一个保存和恢复canvas状态的例子

下面这个例子说明在绘制一系列的矩形的时候状态是如何保存和恢复的。

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        ctx.fillRect(0, 0, 150, 150);   // 使用默认状态绘制一个矩形
        ctx.save();                  // 保存状态

        ctx.fillStyle = '#09F';      // 修改设置
        ctx.fillRect(15, 15, 120, 120); // 使用新设置绘制矩形

        ctx.save();                  // 保存状态
        ctx.fillStyle = '#FFF';      // 修改设置
        ctx.globalAlpha = 0.5;
        ctx.fillRect(30, 30, 90, 90);   // 使用新设置绘制矩形

        ctx.restore();               // 恢复之前状态
        ctx.fillRect(45, 45, 60, 60);   // 使用恢复之后的状态绘图

        ctx.restore();               // 再次恢复之前状态
        ctx.fillRect(60, 60, 30, 30);   // 使用恢复之后的状态绘图
    }
    draw() 

结果如下:

7.2 移动Translation

先看移动的第一个方法translate()。这个方法可以把canvas从起始位置移动到网格坐标中的不同位置。

translate(x, y):移动canvas在网格上的位置。x坐标表示水平位移,y坐标表示垂直位移。如下图

 

 在做一些变形之前,最好保存canvas状态。调用restore方法从最近使用的场景中恢复canvas状态要比反向转换回到原来状态要容易的多。并且,如果在一个循环中执行translate()移动但没有保存和恢复canvas状态,很有可能因为绘图在canvas边缘外部而丢失调。

一个转化的例子

这个例子说明canvas转化的好处。没有translate()转换,可能所有的矩形都在同一个位置(0, 0)绘图。translate()方法还允许我们自由地将矩形放在画布任何位置,而不必手动调用fillRect()方法来调整坐标位置。这使得它更容易理解和使用。

在下面的例子中,有9次调用fillRect()方法,在两个for循环中分别调用三次。在每次循环中,canvas都会移动一次,画一个矩形,然后canvas状态返回到初始状态。注意得益于translae()转移,每次调用fillRect()都使用相同的坐标来绘图。

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        ctx.save(); // 保存位置
        ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)'; // 设置样式
        ctx.translate(10 + j * 50, 10 + i * 50); // 转移
        ctx.fillRect(0, 0, 25, 25); // 画图
        ctx.restore(); // 从最近的状态中恢复状态
      }
    }
  } 

效果如下:

 

7.3 旋转Rotating

第二个变形是旋转rotate(),使用这个方法可以从源位置开始旋转canvas。

rotate(angle):根据角度顺时针旋转画布

旋转的中心位置总是从canvas的原点开始。如果需要改变旋转中心,需要调用translate()方法来转移。

一个旋转的例子

在这个例子中,我们先使用rotate将矩形从中心位置旋转,然后使用translate()方法将矩形从原点移动位置。

注意:角度转换成弧度的计算公式(Mah.PI/180)*degrees

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    // 保存状态
    ctx.save();
    // 蓝色矩形
    ctx.fillStyle = '#0095DD';
    ctx.fillRect(30, 30, 100, 100);
    ctx.rotate((Math.PI / 180) * 25);
    // 绿色矩形
    ctx.fillStyle = '#4D4E53';
    ctx.fillRect(30, 30, 100, 100);
    ctx.restore();

    // 右边矩形,从中心位置旋转
    ctx.fillStyle = '#0095DD';
    ctx.fillRect(150, 30, 100, 100);

    ctx.translate(200, 80); // 转移位置
                            // x = x + 0.5 * width
                            // y = y + 0.5 * height
    ctx.rotate((Math.PI / 180) * 25); // 旋转
    ctx.translate(-200, -80); // 位置回归

    // 画矩形
    ctx.fillStyle = '#4D4E53';
    ctx.fillRect(150, 30, 100, 100);
  } 

结果如下:

 

7.4 伸缩

下一个变形是伸缩,我们使用伸缩来增加或减少画布网格中的大小单位。使用缩放可以按比例放大或缩小画布。

scale(x, y):水平按x缩放画布单位,垂直按y缩放画布单位。两个参数都是实数,小于1的值将按比例缩小,大于1的值将会放大,值为1则保持相同大小。

x,y的值若为负数可以实现轴镜像(例如,使用translate(0, canvas.height); scale(1, -1)可以实现笛卡尔坐标系统,原点在左下角。)

默认情况下,canvas中一个单位是一像素。如果scale(0.5, 0.5),所有像素都是原来大小的一半,所以图形就是原来一半大小。同理,如果参数设置为2,像素大小是正常大小的2倍,图像将为原来大小的2倍。

一个伸缩的例子

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    // 画一个隽星,并x轴放大19倍,y轴缩放3倍
    ctx.save();
    ctx.scale(10, 3);
    ctx.fillRect(1, 10, 10, 10);
    ctx.restore();

    // 水平镜像
    ctx.scale(-1, 1);
    ctx.font = '48px serif';
    ctx.fillText('MDN', -135, 120);
  } 

结果如下:

 

7.5 变形Transforms

最后一个方法使用transform方法允许对转换矩阵进行修改。

transform(m11, m12, m21, m22, dx, dy):这个方法是将当前的变形矩阵乘以一个基于自身参数的矩形。如果任意一个参数是无限大,变形矩阵也必须被标记为无限大,否则会抛出异常。

这个函数的参数的含义如下:

m11:水平方向的缩放
m12:水平方向的倾斜偏移
m21:竖直方向的倾斜偏移
m22:竖直方向的缩放
dx:水平方向的移动
dy:竖直方向的移动

setTransform(m11, m12, m21, m22, dx, dy):这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用transform方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,这个方法是取消了当前变形,然后设置为指定的变形,一步完成。

resetTransform():重置为当前变形为单位矩阵,它和调用下面的语句行为是一致的:ctx.setTransform(1, 0, 0, 1, 0, 0)

transform/setTransform的例子

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');

        var sin = Math.sin(Math.PI / 6);
        var cos = Math.cos(Math.PI / 6);
        ctx.translate(100, 100);
        var c = 0;
        for (var i = 0; i <= 12; i++) {
            c = Math.floor(255 / 12 * i);
            ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
            ctx.fillRect(0, 0, 100, 10);
            ctx.transform(cos, sin, -sin, cos, 0, 0);
        }

        ctx.setTransform(-1, 0, 0, 1, 100, 100);
        ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
        ctx.fillRect(0, 50, 100, 100);
    } 

结果如下:

 

8. 合成与裁剪

上面的例子中,我们总是将一个图形画在另一个图形纸上,对于更多的情况,仅仅这样是不够的。比如对于合成图形来说,绘制顺序会有限制。不过我们可以用globalCompositeOperation属性来改变这种情况。此外clip属性允许我们隐藏不想看到的部分。

8.1 globalCompositeOperation

不仅可以在已有图形后面画新图形,还可以用来遮盖指定区域,清除画布中的某些部分(清除不仅限于矩形,像clearRect()方法做的那样)以及其他的操作。

globalCompositeOperation=type:这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串。

下面看一个综合的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Compositing</title>
</head>
<body>
<script>
    // 全局变量
    var canvas1 = document.createElement("canvas");
    var canvas2 = document.createElement("canvas");
    // 页面元素
    var gco = ['source-over', 'source-in', 'source-out', 'source-atop',
        'destination-over', 'destination-in', 'destination-out', 'destination-atop',
        'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken',
        'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
        'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'].reverse();
    var gcoText = [
        '这是默认设置,并在现有画布上下文之上绘制新图形。',
        '新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。',
        '在不与现有画布内容重叠的地方绘制新图形。',
        '新图形只在与现有画布内容重叠的地方绘制。',
        '在现有的画布内容后面绘制新的图形。',
        '现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。',
        '现有内容保持在新图形不重叠的地方。',
        '现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。',
        '两个重叠图形的颜色是通过颜色值相加来确定的。',
        '只显示新图形。',
        '图像中,那些重叠和正常绘制之外的其他地方是透明的。',
        '将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。',
        '像素被倒转,相乘,再倒转,结果是一幅更明亮的图片。',
        'multiply和screen的结合,原本暗的地方更暗,原本亮的地方更亮。',
        '保留两个图层中最暗的像素。',
        '保留两个图层中最亮的像素。',
        '将底层除以顶层的反置。',
        '将反置的底层除以顶层,然后将结果反过来。',
        '屏幕相乘(A combination of multiply and screen)类似于叠加,但上下图层互换了。',
        '用顶层减去底层或者相反来得到一个正值。',
        '一个柔和版本的强光(hard-light)。纯黑或纯白不会导致纯黑或纯白。',
        '和difference相似,但对比度较低。',
        '保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue)。',
        '保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma)。',
        '保留了底层的亮度(luma),同时采用了顶层的色调(hue)和色度(chroma)。',
        '保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma)。'].reverse();
    // 全局尺寸
    var width = 320;
    var height = 340;
    // 页面加载事件,页面主程序
    window.onload = function () {
        var lum = {
            r: 0.33,
            g: 0.33,
            b: 0.33
        };
        // 设置canvas尺寸
        canvas1.width = width;
        canvas1.height = height;
        canvas2.width = width;
        canvas2.height = height;
        lightMix()
        colorSphere();
        runComposite();
        return;
    };
    // 创建canvas
    function createCanvas() {
        var canvas = document.createElement("canvas");
        canvas.style.background = "url(" + op_8x8.data + ")";
        canvas.style.border = "1px solid #000";
        canvas.style.margin = "5px";
        canvas.width = width / 2;
        canvas.height = height / 2;
        return canvas;
    }
    //
    function runComposite() {
        var dl = document.createElement("dl");
        document.body.appendChild(dl);
        while (gco.length) {
            var pop = gco.pop();
            var dt = document.createElement("dt");
            dt.textContent = pop;
            dl.appendChild(dt);
            var dd = document.createElement("dd");
            var p = document.createElement("p");
            p.textContent = gcoText.pop();
            dd.appendChild(p);

            var canvasToDrawOn = createCanvas();
            var canvasToDrawFrom = createCanvas();
            var canvasToDrawResult = createCanvas();

            var ctx = canvasToDrawResult.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas1, 0, 0, width / 2, height / 2);
            ctx.globalCompositeOperation = pop;
            ctx.drawImage(canvas2, 0, 0, width / 2, height / 2);
            ctx.globalCompositeOperation = "source-over";
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText(pop, 5, height / 2 - 5);
            ctx.restore();

            var ctx = canvasToDrawOn.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas1, 0, 0, width / 2, height / 2);
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText('existing content', 5, height / 2 - 5);
            ctx.restore();

            var ctx = canvasToDrawFrom.getContext('2d');
            ctx.clearRect(0, 0, width, height)
            ctx.save();
            ctx.drawImage(canvas2, 0, 0, width / 2, height / 2);
            ctx.fillStyle = "rgba(0,0,0,0.8)";
            ctx.fillRect(0, height / 2 - 20, width / 2, 20);
            ctx.fillStyle = "#FFF";
            ctx.font = "14px arial";
            ctx.fillText('new content', 5, height / 2 - 5);
            ctx.restore();

            dd.appendChild(canvasToDrawOn);
            dd.appendChild(canvasToDrawFrom);
            dd.appendChild(canvasToDrawResult);

            dl.appendChild(dd);
        }
    };
    // 公用函数
    var lightMix = function () {
        var ctx = canvas2.getContext("2d");
        ctx.save();
        ctx.globalCompositeOperation = "lighter";
        ctx.beginPath();
        ctx.fillStyle = "rgba(255,0,0,1)";
        ctx.arc(100, 200, 100, Math.PI * 2, 0, false);
        ctx.fill()
        ctx.beginPath();
        ctx.fillStyle = "rgba(0,0,255,1)";
        ctx.arc(220, 200, 100, Math.PI * 2, 0, false);
        ctx.fill()
        ctx.beginPath();
        ctx.fillStyle = "rgba(0,255,0,1)";
        ctx.arc(160, 100, 100, Math.PI * 2, 0, false);
        ctx.fill();
        ctx.restore();
        ctx.beginPath();
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 30, 30)
        ctx.fill();
    };
    // 公用函数
    var colorSphere = function (element) {
        var ctx = canvas1.getContext("2d");
        var width = 360;
        var halfWidth = width / 2;
        var rotate = (1 / 360) * Math.PI * 2; // per degree
        var offset = 0; // scrollbar offset
        var oleft = -20;
        var otop = -20;
        for (var n = 0; n <= 359; n++) {
            var gradient = ctx.createLinearGradient(oleft + halfWidth, otop, oleft + halfWidth, otop + halfWidth);
            var color = Color.HSV_RGB({H: (n + 300) % 360, S: 100, V: 100});
            gradient.addColorStop(0, "rgba(0,0,0,0)");
            gradient.addColorStop(0.7, "rgba(" + color.R + "," + color.G + "," + color.B + ",1)");
            gradient.addColorStop(1, "rgba(255,255,255,1)");
            ctx.beginPath();
            ctx.moveTo(oleft + halfWidth, otop);
            ctx.lineTo(oleft + halfWidth, otop + halfWidth);
            ctx.lineTo(oleft + halfWidth + 6, otop);
            ctx.fillStyle = gradient;
            ctx.fill();
            ctx.translate(oleft + halfWidth, otop + halfWidth);
            ctx.rotate(rotate);
            ctx.translate(-(oleft + halfWidth), -(otop + halfWidth));
        }
        ctx.beginPath();
        ctx.fillStyle = "#00f";
        ctx.fillRect(15, 15, 30, 30)
        ctx.fill();
        return ctx.canvas;
    };
    // 计算颜色 HSV (1978) = H: Hue / S: Saturation / V: Value
    Color = {};
    Color.HSV_RGB = function (o) {
        var H = o.H / 360,
            S = o.S / 100,
            V = o.V / 100,
            R, G, B;
        var A, B, C, D;
        if (S === 0) {
            R = G = B = Math.round(V * 255);
        } else {
            if (H >= 1) H = 0;
            H = 6 * H;
            D = H - Math.floor(H);
            A = Math.round(255 * V * (1 - S));
            B = Math.round(255 * V * (1 - (S * D)));
            C = Math.round(255 * V * (1 - (S * (1 - D))));
            V = Math.round(255 * V);
            switch (Math.floor(H)) {
                case 0:
                    R = V;
                    G = C;
                    B = A;
                    break;
                case 1:
                    R = B;
                    G = V;
                    B = A;
                    break;
                case 2:
                    R = A;
                    G = V;
                    B = C;
                    break;
                case 3:
                    R = A;
                    G = B;
                    B = V;
                    break;
                case 4:
                    R = C;
                    G = A;
                    B = V;
                    break;
                case 5:
                    R = V;
                    G = A;
                    B = B;
                    break;
            }
        }
        return {
            R: R,
            G: G,
            B: B
        };
    };
    var createInterlace = function (size, color1, color2) {
        var proto = document.createElement("canvas").getContext("2d");
        proto.canvas.width = size * 2;
        proto.canvas.height = size * 2;
        proto.fillStyle = color1; // top-left
        proto.fillRect(0, 0, size, size);
        proto.fillStyle = color2; // top-right
        proto.fillRect(size, 0, size, size);
        proto.fillStyle = color2; // bottom-left
        proto.fillRect(0, size, size, size);
        proto.fillStyle = color1; // bottom-right
        proto.fillRect(size, size, size, size);
        var pattern = proto.createPattern(proto.canvas, "repeat");
        pattern.data = proto.canvas.toDataURL();
        return pattern;
    };
    var op_8x8 = createInterlace(8, "#FFF", "#eee");
</script>
</body>
</html> 

8.2 剪切路径

剪切路径和普通的canvas图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。如下图所示。红色边的五角星就是剪切路径,所有在路径意外的部分不会在canvas上绘制出来。

 

如果和上面介绍的globalCompositeOperation属性比较,它可以实现和source-in和source-atop差不多的效果。区别是剪切路径不会在canvas上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里会制图型很好用。

在上面绘制图形一节里,介绍了stroke和fill方法,这里介绍第三种方法clip。

clip():将当前正在构建的路径转换为当前的裁剪路径。

我们使用clip()方法来创建一个新的裁剪路径。

默认情况下,canvas有一个与它自身一样大的剪切路径(也就是没有剪切效果)。

clip的例子

在这个例子里,用一个圆形的剪切路径来限制随机星星的绘制区域。首先画一个和canvas一样大小的黑色矩形作为背景,然后移动原点至中心点。然后使用clip方法创建一个弧形的剪切路径。剪切路径也属于canvas状态的一部分,可以被保存起来。代码如下:

    function draw() {
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.fillRect(0, 0, 150, 150);
        ctx.translate(75, 75);

        // Create a circular clipping path
        ctx.beginPath();
        ctx.arc(0, 0, 60, 0, Math.PI * 2, true);
        ctx.clip();

        // draw background
        var lingrad = ctx.createLinearGradient(0, -75, 0, 75);
        lingrad.addColorStop(0, '#232256');
        lingrad.addColorStop(1, '#143778');

        ctx.fillStyle = lingrad;
        ctx.fillRect(-75, -75, 150, 150);

        // draw stars
        for (var j = 1; j < 50; j++) {
            ctx.save();
            ctx.fillStyle = '#fff';
            ctx.translate(75 - Math.floor(Math.random() * 150),
                75 - Math.floor(Math.random() * 150));
            drawStar(ctx, Math.floor(Math.random() * 4) + 2);
            ctx.restore();
        }

    }

    function drawStar(ctx, r) {
        ctx.save();
        ctx.beginPath()
        ctx.moveTo(r, 0);
        for (var i = 0; i < 9; i++) {
            ctx.rotate(Math.PI / 5);
            if (i % 2 == 0) {
                ctx.lineTo((r / 0.525731) * 0.200811, 0);
            } else {
                ctx.lineTo(r, 0);
            }
        }
        ctx.closePath();
        ctx.fill();
        ctx.restore();
    } 

效果如下:

 

如果我们在创建新裁剪路径时想要保存原来的裁剪路径,我们需要做的就是保存一下canvas的状态。

简介路径创建之后所有出现在它里面的东西才会画出来。在画线性渐变时我们就会注意到这一点。然后绘制处50颗随机分布的(经过缩放)星星,让然也只有才裁剪路径里的星星才能绘制出来。

9. 基本的动画

由于我们是通过JavaScript去操控<canvas>对象,这样实现一些交互动画是很容易的,这里先了解一些基本的动画。

canvas动画最大的限制是图像一旦绘制出来,它就一直保持这个状态。如果要移动它,就不得不对所有的东西(包含之前的)进行重绘。重绘是相当费时的,而且性能很依赖于电脑的速度。

9.1 动画的基本步骤

可以通过一下的步骤来画出一帧:

  1. 清空canvas:除非接下来要画的内容会完全填充canvas(例如背景图),否则需要清空所有。最简单的做法就是调用clearRect方法。
  2. 保存canvas状态:如果改变一些会影响canvas状态的设置(例如样式,变形等),并且在绘制每帧的时候保存初始状态,则需要保存原始状态。
  3. 画动画的形状:进行实际帧绘制的步骤。
  4. 重置canvas状态:如果已经保存了canvas的状态,可以先恢复它,然后重绘一下。

 9.2 操控动画

在canvas上绘制内容是用canvas提供的或者自定义的方法,通常,我们仅仅在脚本执行结束后才能看到结果,比如说在for循环中完成动画是不太可能的。因此为了实现动画,需要一些可以定时执行重绘的方法。有两种方法可以实现这样的动画操控。首先可以通过setInterval和setTimeout方法来绘制在设定的时间点上执行的重绘。

预定更新

有三个函数可以在预定时间段后执行一个指定的函数:window.setInterval(),window.setTimeout(),window.requestAnimationFrame()。

  • setInterval(function, delay):每延迟delay指定的毫秒数,就执行指定的函数function
  • setTimeout(function, delay):延迟delay指定的毫秒数后开始执行函数function
  • requestAnimationFrame(callback):高速浏览器希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画

如果不需要和用户互动,可以使用setInterval方法,它可以定期执行指定的代码。如果需要做一个游戏,可以使用键盘或者鼠标事件配合setTimeout来实现。通过设置监听事件,可以捕捉用户的交互,并执行相应的动作。

下面的动画,采用window.requestAnimationFrame()实现动画效果。这个方法提供了更加平缓并且更加有效的方式执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒回调函数执行60次,也有可能被降低。

9.3 太阳系的动画

在这个例子里,我们做一个小型的太阳系模拟动画:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>anmation</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
  var sun = new Image();
  var moon = new Image();
  var earth = new Image();

  function init() {
    sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
    moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
    earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
    window.requestAnimationFrame(draw);
  }

  function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');

    ctx.globalCompositeOperation = 'destination-over';
    ctx.clearRect(0, 0, 300, 300); // clear canvas

    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.strokeStyle = 'rgba(0,153,255,0.4)';
    ctx.save();
    ctx.translate(150, 150);

    // 地球
    var time = new Date();
    ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds());
    ctx.translate(105, 0);
    ctx.fillRect(0, -12, 50, 24); // 阴影
    ctx.drawImage(earth, -12, -12);

    // Moon
    ctx.save();
    ctx.rotate(((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds());
    ctx.translate(0, 28.5);
    ctx.drawImage(moon, -3.5, -3.5);
    ctx.restore();

    ctx.restore();

    ctx.beginPath();
    ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // 地球轨道
    ctx.stroke();

    ctx.drawImage(sun, 0, 0, 300, 300);

    window.requestAnimationFrame(draw);
  }

  init();
</script>
</body>
</html> 

结果如下:

9.4 一个时钟动画

下面这个动画实现一个时钟,展示当前的时间。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>clock</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
    function clock() {
        var now = new Date();
        var ctx = document.getElementById('canvas').getContext('2d');
        ctx.save();
        ctx.clearRect(0, 0, 150, 150);
        ctx.translate(75, 75);
        ctx.scale(0.4, 0.4);
        ctx.rotate(-Math.PI / 2);
        ctx.strokeStyle = 'black';
        ctx.fillStyle = 'white';
        ctx.lineWidth = 8;
        ctx.lineCap = 'round';

        // 时针
        ctx.save();
        for (var i = 0; i < 12; i++) {
            ctx.beginPath();
            ctx.rotate(Math.PI / 6);
            ctx.moveTo(100, 0);
            ctx.lineTo(120, 0);
            ctx.stroke();
        }
        ctx.restore();

        // 分针
        ctx.save();
        ctx.lineWidth = 5;
        for (i = 0; i < 60; i++) {
            if (i % 5 !== 0) {
                ctx.beginPath();
                ctx.moveTo(117, 0);
                ctx.lineTo(120, 0);
                ctx.stroke();
            }
            ctx.rotate(Math.PI / 30);
        }
        ctx.restore();

        var sec = now.getSeconds();
        var min = now.getMinutes();
        var hr = now.getHours();
        hr = hr >= 12 ? hr - 12 : hr;

        ctx.fillStyle = 'black';

        // 画时针
        ctx.save();
        ctx.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec);
        ctx.lineWidth = 14;
        ctx.beginPath();
        ctx.moveTo(-20, 0);
        ctx.lineTo(80, 0);
        ctx.stroke();
        ctx.restore();

        // 画分针
        ctx.save();
        ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
        ctx.lineWidth = 10;
        ctx.beginPath();
        ctx.moveTo(-28, 0);
        ctx.lineTo(112, 0);
        ctx.stroke();
        ctx.restore();

        // 画秒针
        ctx.save();
        ctx.rotate(sec * Math.PI / 30);
        ctx.strokeStyle = '#D40000';
        ctx.fillStyle = '#D40000';
        ctx.lineWidth = 6;
        ctx.beginPath();
        ctx.moveTo(-30, 0);
        ctx.lineTo(83, 0);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
        ctx.stroke();
        ctx.fillStyle = 'rgba(0, 0, 0, 0)';
        ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
        ctx.fill();
        ctx.restore();

        ctx.beginPath();
        ctx.lineWidth = 14;
        ctx.strokeStyle = '#325FA2';
        ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.restore();

        window.requestAnimationFrame(clock);
    }

    window.requestAnimationFrame(clock);
</script>
</body>
</html> 

效果如下:

9.4 一个全景图的例子

在这个例子中实现一个从左到右滚动的全景图。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="800" height="300"></canvas>
<script>
    var img = new Image();

    // 全局变量
    img.src = 'https://mdn.mozillademos.org/files/4553/Capitan_Meadows,_Yosemite_National_Park.jpg';
    var CanvasXSize = 800;
    var CanvasYSize = 200;
    var speed = 30; // 播放速度
    var scale = 1.05;
    var y = -4.5; // 水平位移

    // 主程序
    var dx = 0.75;
    var imgW;
    var imgH;
    var x = 0;
    var clearX;
    var clearY;
    var ctx;

    img.onload = function() {
        imgW = img.width * scale;
        imgH = img.height * scale;

        if (imgW > CanvasXSize) {
            // 如果图片比canvas大
            x = CanvasXSize - imgW;
        }
        if (imgW > CanvasXSize) {
            // 图片比canvas宽
            clearX = imgW;
        } else {
            clearX = CanvasXSize;
        }
        if (imgH > CanvasYSize) {
            // 图片比canvas宽
            clearY = imgH;
        } else {
            clearY = CanvasYSize;
        }

        // 获取canvas
        ctx = document.getElementById('canvas').getContext('2d');

        // 设置播放速度
        return setInterval(draw, speed);
    }

    function draw() {
        ctx.clearRect(0, 0, clearX, clearY); // 清空

        // 如果图片比canvas小
        if (imgW <= CanvasXSize) {
            // 重置
            if (x > CanvasXSize) {
                x = -imgW + x;
            }
            // 画额外的图像1
            if (x > 0) {
                ctx.drawImage(img, -imgW + x, y, imgW, imgH);
            }
            // 画额外的图像2
            if (x - imgW > 0) {
                ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
            }
        }

        // 图像比canvas大
        else {
            // 重置
            if (x > (CanvasXSize)) {
                x = CanvasXSize - imgW;
            }
            // 画额外图像
            if (x > (CanvasXSize-imgW)) {
                ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
            }
        }
        // 画图像
        ctx.drawImage(img, x, y,imgW, imgH);
        // 移动
        x += dx;
    }
</script>

</body>
</html> 

结果如下: 

10. 高级动画

上面我们制作了基本动画以及了解让绘图移动的方法,这一部分将会对动画有更深入的了解,并对运动有更深入的了解,并添加一些物理轨迹让动画看起来更加高级。

10.1 绘制小球

这里绘制一个小球用于动画学习。下面代码先先建立一个画布。

<canvas id="canvas" width="600" height="300"></canvas> 

首先要画一个context(画布场景) 。为了画处这个球,再创建一个包含一些相关属性以及drwa()函数的ball对象来完成绘制。

    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');

    var ball = {
        x: 100,
        y: 100,
        radius: 25,
        color: 'blue',
        draw: function() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fillStyle = this.color;
            ctx.fill();
        }
    };
    ball.draw(); 

这里没有什么特别的,小球实际上是一个简单的圆形,使用arc函数画出。

10.2 添加速率

有了小球,现在添加一些基本动画,这里使用上面提到的window.requestAnimationFrame()方法,控制动画。小球依旧靠添加速度矢量进行移动。在每一帧里,依旧使用clear清理掉之前帧里旧的圆形。

    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var raf = null;

    var ball = {
        x: 100,
        y: 100,
        vx: 5,
        vy: 2,
        radius: 25,
        color: 'blue',
        draw: function() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fillStyle = this.color;
            ctx.fill();
        }
    };

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ball.draw();
        ball.x += ball.vx;
        ball.y += ball.vy;
        raf = window.requestAnimationFrame(draw)
    }

    canvas.addEventListener("mouseover", function (e) {
        raf = window.requestAnimationFrame(draw)
    })

    canvas.addEventListener("mouseout", function (e) {
        window.cancelAnimationFrame(raf);
    })

    ball.draw(); 

10.3 边界

如果没有任何碰撞检测,小球很快就会超出画布。所以需要检查小球的x和y位置是否超出画布尺寸以及是否需要将速度矢量翻转。为了这么做,需要把下面的检查代码添加进入draw函数中:

if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
  ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
  ball.vx = -ball.vx;
} 

预览一下,包鼠标放在canvas范围内部,效果是这样的:

10.4 加速度

为了使动作效果看起来更加真实,可以像这样处理速度,例如:

ball.vy *= .99;
ball.vy += .25; 

这会减少垂直方向上的速度,所以小球只会在地板上弹跳。

10.5 长尾效果

现在,我们使用的是clearRect函数帮助我们清除前一帧动画。如果用一个半透明的fillRect函数取而代之,就可以轻松实现长尾效果。

ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height); 

效果如下:

10.6 添加鼠标控制

为了更好的控制小球,我们可以使用mousemove事件让它跟随鼠标活动。下面例子中,click事件会释放小球然后让它重新跳起。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;
var running = false;
var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 1,
  radius: 25,
  color: 'blue',
  draw: function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
};
function clear() {
  ctx.fillStyle = 'rgba(255,255,255,0.3)';
  ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
  clear();
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener('mousemove', function(e){
  if (!running) {
    clear();
    ball.x = e.clientX;
    ball.y = e.clientY;
    ball.draw();
  }
});
canvas.addEventListener('click',function(e){
  if (!running) {
    raf = window.requestAnimationFrame(draw);
    running = true;
  }
});
canvas.addEventListener('mouseout', function(e){
  window.cancelAnimationFrame(raf);
  running = false;
});
ball.draw(); 

11. 像素操作

到目前为止,还没有介绍canvas画布真是像素原理,事实上可以通过ImageData对象操作像素数据,直接读取或将数据写入该对象中。稍后将详细介绍如何控制图像使其平滑(非锯齿)以及如何从canvas画布中保存图像。

11.1 ImageData对象

ImageData对象中存储着canvas对象的像素数据,它包含下面几个只读属性:

  • width:图片宽度,单位是像素
  • height:图片高度,单位是像素
  • data:Uint8ClampedArray类型的一维数组,包含着RGBA格式的证书数据,范围在0至255之间(包含255)

data属性返回一个Uint8ClampeArray,它可以被使用作为查看初始像素数据。每个像素使用4个1bytes值(按照红,绿,蓝和透明值的顺序,就是rgba格式)来代表。每个颜色值部分用0至255来代表。每个部分被分配到一个在数组内连续的索引,左上角像素的红色部分在数组索引0为止。像素从左到右被处理,然后往下,遍历整个数组。

Uint8ClampedArray包含高度 X 宽度 X 4bytes数据,索引值从0到(高度X宽度X4)- 1,例如要读取图片中位于第50行,第200列的像素的蓝色部分,代码如下:

blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2]; 

根据行,列读取某像素点的RGBA值的公式:

imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3]; 

可能会使用Uint8ClampedArray.length属性来读取像素数组的大小(以bytes为单位):

var numBytes = imageData.data.length; 

11.2 得到场景像素数据

为了获得一个包含为了获得一个包含画布场景像素数据的ImageData对象,可以使用getImage()方法。

var myImageData = ctx.getImageData(left, top, width, height); 

这个方法返回一个ImageData对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为(left, top), (left + width, top),(left, top + height)以及(left + width, top + height)四个点。这些坐标点呗设定为画布坐标空间元素。

注意:任何画布意外的元素都被返回成一个透明的ImageData对象

颜色选择器

在这个例子中,我们会使用getImageData()去展示鼠标光标下的颜色。为此,我们需要得到当前鼠标的位置,记为layerX和layerY,然后去查询getImageData()给我们提供的在哪个位置的像素数组里面的像素数据。最后我们使用数组数据去设置背景颜色和div的文字颜色。注意这里不能使用远程图片,不然会报错:Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="300" height="200"></canvas>
<div id="color" style="width: 300px; height: 200px;"></div>
<script>
  var img = new Image();
  // img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
  img.src = './../images/rhino.jpg';
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  img.onload = function () {
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
  };
  var color = document.getElementById('color');

  function pick(event) {
    var x = event.layerX;
    var y = event.layerY;
    var pixel = ctx.getImageData(x, y, 1, 1);
    var data = pixel.data;
    var rgba = 'rgba(' + data[0] + ', ' + data[1] +
      ', ' + data[2] + ', ' + (data[3] / 255) + ')';
    color.style.background = rgba;
    color.textContent = rgba;
  }
  canvas.addEventListener('mousemove', pick);
</script>
</body>
</html> 

 效果如下:

11.3 在canvas画布中写入像素数据

可以使用putImageData()方法在canvas的画布中写入一个像素数据。

ctx.putImageData(myImageData, dx, dy); 

参数dx,dy表示要绘制的像素点在坐标系中x轴和y轴距离。举例,下面代码在坐标左上角位置画一个点。

ctx.putImageData(myImageData, 0, 0); 

灰度变换和反转颜色

在这个例子中,遍历所有像素以更改它们的值,然后使用putImageData()将修改后的像素画在画布上。invert函数只是简单的用255减去当前颜色值。grayscale函数只是简单的使用红色,绿色,蓝色值的平均值。还可以使用一个加权平均值,例如:公式x = 0.299r + 0.587g + 0.114b

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="300" height="200"></canvas><br/>
<button id="grayscalebtn">GrayScale</button>
<button id="invertbtn">Invert</button>

<script>
    var img = new Image();
    img.src = './../images/rhino.jpg';
    img.onload = function () {
        draw(this);
    };

    function draw(img) {
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        img.style.display = 'none';
        var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        var data = imageData.data;
        var invert = function () {
            for (var i = 0; i < data.length; i += 4) {
                data[i] = 255 - data[i];            // 红色
                data[i + 1] = 255 - data[i + 1];    // 绿色
                data[i + 2] = 255 - data[i + 2];    // 蓝色
            }
            ctx.putImageData(imageData, 0, 0);
        };
        var grayscale = function () {
            for (var i = 0; i < data.length; i += 4) {
                var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
                data[i] = avg; // red
                data[i + 1] = avg; // green
                data[i + 2] = avg; // blue
            }
            ctx.putImageData(imageData, 0, 0);
        };
        var invertbtn = document.getElementById('invertbtn');
        invertbtn.addEventListener('click', invert);
        var grayscalebtn = document.getElementById('grayscalebtn');
        grayscalebtn.addEventListener('click', grayscale);
    }
</script>
</body>
</html> 

效果如下:

11.4 缩放和抗锯齿

 在drawImage()方法,画布重叠和imageSmoothingEnable属性的帮助下,我们可以放大显示图片以及看到的详细内容。

我们得到鼠标的位置并裁减出距左和上5像素,距右下5像素的图片。然后我们将这幅图复制到另一个画布,并将图片调整到我们想要的大小。在缩放画布里,我们将10X10像素的对原画布裁减调整为20X20,代码如下:

zoomctx.drawImage(canvas, 
                  Math.abs(x - 5), Math.abs(y - 5),
                  10, 10, 0, 0, 200, 200); 

因为反锯齿是默认启用的,我们可能想要关闭它以看到清楚的像素。可以通过切换CheckBox来看到imageSmoothingEnable属性的效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>imageSmoothingEnabled</title>
</head>
<body>
<canvas id="canvas" width="200" height="200"></canvas>
<canvas id="zoom" width="200" height="200"></canvas>
<br/>
<input type="checkbox" id="smoothbtn"/><label for="smoothbtn">Enable image smoothing</label>
<script>
  var img = new Image();
  img.src = './../images/rhino.jpg';
  img.onload = function () {
    draw(this);
  };
  function draw(img) {
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
    var zoomctx = document.getElementById('zoom').getContext('2d');

    var smoothbtn = document.getElementById('smoothbtn');
    var toggleSmoothing = function (event) {
      zoomctx.imageSmoothingEnabled = this.checked;
      zoomctx.mozImageSmoothingEnabled = this.checked;
      zoomctx.webkitImageSmoothingEnabled = this.checked;
      zoomctx.msImageSmoothingEnabled = this.checked;
    };
    smoothbtn.addEventListener('change', toggleSmoothing);

    var zoom = function (event) {
      var x = event.layerX;
      var y = event.layerY;
      zoomctx.drawImage(canvas,
        Math.abs(x - 5),
        Math.abs(y - 5),
        10, 10,
        0, 0,
        200, 200);
    };
    canvas.addEventListener('mousemove', zoom);
  }
</script>
</body>
</html> 

效果如下:

 

11.5 保存图片

 HTMLCanvasElement提供一个toDataUrl()方法,此方法在保存图片的时候非常有用。它返回一个包含类型参数规定的图像个数的数据链接。图片分辨率是96dpi。

canvas.toDataUrl('image/png'):默认设定,创建一个png图片。
canvas.toDataUrl('image/jpeg', quality):创建一个jpg图片,可以设置第二个参数,从0到1,0表示图片比较粗糙,但是文件比较小,1表示品质最好。当从画布中生成一个数据链接,例如可以将它放在任何<image>元素,或者将它放在一个有download属性的超链接里用于保存到本地。

也可以从画布中创建一个Blob对象。

canvas.toBlob(callback, type, encoderOptions):从画布图片中保存Blob对象。

12. 点击区域和无障碍访问

<canvas>标签只是一个位图,不提供任何已经绘制在上面的对象的信息。canvas的内容不能像语义化的HTML一样暴露一些协助工具。一般来说,应该避免在交互类型的网站或者App上使用canvas。下面讨论的内容可以让canvas交互更加容易。

12.1 内容兼容

<canvas></canvas>标签里的内容可以被一些不支持canvas的浏览器提供兼容。这对残疾用户设备很有用(比如屏幕阅读器),这样它们可以读取并解释DOM里的子节点。

<canvas> 
  <h2>Shapes</h2> 
  <p>A rectangle with a black border. 
   In the background is a pink circle. 
   Partially overlaying the <a href="http://en.wikipedia.org/wiki/Circle" onfocus="drawCircle();" onblur="drawPicture();">circle</a>. 
   Partially overlaying the circle is a green 
   <a href="http://en.wikipedia.org/wiki/Square" onfocus="drawSquare();" onblur="drawPicture();">square</a> 
   and a purple <a href="http://en.wikipedia.org/wiki/Triangle" onfocus="drawTriangle();" onblur="drawPicture();">triangle</a>,
   both of which are semi-opaque, so the full circle can be seen underneath.</p> 
</canvas>  

12.2 ARIA规则

Accessible Rich Internet Applications (ARIA) 定义了让Web内容和Web应用更容易被有身体缺陷的人获取的办法。你可以用ARIA属性来描述canvas元素的行为和存在目的。详情见ARIA和 ARIA 技术。

12.3 点击区域(hit region)

判断鼠标坐标是否在canvas上一个特定区域里一直是个有待解决的问题。hit region API让你可以在canvas上定义一个区域,这让无障碍工具获取canvas上的交互内容成为可能。它能让你更容易地进行点击检测并把事件转发到DOM元素去。这个API有以下三个方法(都是实验性特性,请先在浏览器兼容表上确认再使用)

CanvasRenderingContext2D.addHitRegion():在canvas上添加一个点击区域。
CanvasRenderingContext2D.removeHitRegion():在canvas上移除指定id的点击区域。
CanvasRenderingContext2D.clearHitRegion():移除canvas上所有点击区域。

可以把一个点击区域添加到路径里并检测MouseEvent.region属性来测试鼠标有没有点击这个区域。

<canvas id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.arc(70, 80, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.addHitRegion({id: "circle"});

canvas.addEventListener("mousemove", function(event){
  if(event.region) {
    alert("hit region: " + event.region);
  }
});
</script> 

很不幸,当前chrome版本并不支持这个试验中的特性。

 

 

 

addHitRegion()方法可以带一个control选项可以把事件转发到指定的元素(canvas里的)上。

ctx.addHitRegion({control: element}); 

12.4 焦点圈

当用键盘控制时,焦点圈是一个可以帮我们在页面上快速导航的标记。要在canvas上绘制焦点圈,可以使用drawFocusIfNeeded属性。

CanvasRenderingContext2D.drawFocusIfNeeded():如果给定的的元素获取了焦点,这个方法会当前路径画焦点圈。另外,scroolPathIntoView()可以让一个元素获得焦点的时候在屏幕上可见(滚动到元素所在的区域) 。
CanvasRenderingContext2D.scrollPathIntoView():把当前路径或者一个指定的路径滚动到显示区域内。

13. canvas的优化

<canvas>元素是众多网络2D图像渲染标准之一。被广泛应用于游戏及复杂图像可视化中。然而,随着网站和应用将canvas网布越来越复杂,性能开始成为问题。这里有一些canvas画布元素优化的建议,保证使用时渲染性能。

13.1 性能优化建议

在离屏canvas上预渲染想死的图形或者重复对象

如果发现每一帧里有好多复杂的图画运算,可以考虑建立一个离屏canvas,将图像在这个画布上画一次(或者每当图像改变时画一次),然后在每帧上画处视线以外的这个画布。

myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");
myEntity.render(myEntity.offscreenContext); 

避免使用浮点数的坐标点,使用整数

当画一个有浮点数的对象时,会发生子像素渲染。

ctx.drawImage(myImage, 0.3, 0.5); 

浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,保证在调用drawImage()函数时,使用Math.floor()函数对所有坐标点取整。 

不要在drawImage时缩放图像

有些动画会出现这种情况,一部分元素不断地改变或者移动,而其他的元素,例如外观,永远不变。这种情况的一种优化方法是用多个画布创建不同的层次。

例如:可以在最顶层创建一个外观层,而且仅仅在用户输入的时候被画出。可以创建一个游戏层,在这个游戏层上面会有不断更新的元素和一个背景层,用于绘制那些较少更新的元素。

<div id="stage">
  <canvas id="ui-layer" width="480" height="320"></canvas>
  <canvas id="game-layer" width="480" height="320"></canvas>
  <canvas id="background-layer" width="480" height="320"></canvas>
</div>
 
<style>
  #stage {
    width: 480px;
    height: 320px;
    position: relative;
    border: 2px solid black
  }
  canvas { position: absolute; }
  #ui-layer { z-index: 3 }
  #game-layer { z-index: 2 }
  #background-layer { z-index: 1 }
</style> 

使用CSS设置大的背景图 

如果像大多数游戏那样,有一个静态的背景,可以使用一个div元素,给background特性设置图片,以及将它置于画布元素之后。了这么做可以避免在每一帧画布上绘制大图。

使用CSS transforms特性缩放画布

CSS Transform特性由于调用GPU,因此更快捷。最好的情况是,不要将小画布放大,而是去将大画布缩小。例如Firefox系统,目标分辨率是480X320px。

var scaleX = canvas.width / window.innerWidth;
var scaleY = canvas.height / window.innerHeight;

var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);

stage.style.transformOrigin = '0 0'; //scale from top left
stage.style.transform = 'scale(' + scaleToFit + ')'; 

关闭透明度

如果游戏使用画布而且不需要透明,可以使用HtmlElement.getContext()创建一个绘制上下文把alpha选项设置为false。这个选项可以帮助浏览器进行内部优化。

var ctx = canvas.getContext('2d', { alpha: false }); 

其他优化建议

  • 将画布的函数调用集合到一起(例如:画一条折线,而不要画多条分开的直线)
  • 避免不必要的画布状态改变
  • 渲染画布中不同点,而非整个更新状态
  • 尽量避免使用shadowBlur特性
  • 尽量避免text rendering
  • 使用不同的办法清除画布(clearRect(),fillRect(),刁征canvas大小)
  • 使用windows.requestAnimationFrame()调用动画,而非window.setInterval() 

 

posted @ 2020-04-13 11:59  nd  阅读(460)  评论(0编辑  收藏