首页 > 技术文章 > H5canvas基础

coolslider 2016-02-29 15:01 原文

本篇文章开始讲解HTML5的核心功能之一:Canvas

通过Canvas可以动态生成和展示图形、图表、图像以及动画。

Canvas API功能非常多,我们将讨论最常用的功能。

我们先新建一个canvas看看。

我们给canvas加一个边框,这样比较方便看。

可以看到, canvas会创建一块矩形区域,默认情况下生成大小是300*150像素。

在页面中加入canvas后,我们便可以通过js来自由地控制她。

例如 添加图片、线条以及文字,也可以在里面绘图,甚至加入高级动画。

Note

把canvas当作一个普通的标签,可以通过应用CSS的方式来改变样式,而且一些CSS属性还可以被canvas内的元素继承。

例如字体样式,在canvas内添加文字,其样式默认是同canvas元素本身是一样的。

文章提纲

  • 要点
  • 理论基础/前置条件
  • 详细步骤
  • 总结

要点

掌握使用canvas API的重要流程

掌握常用的canvas API:例如moveTo, lineTo, beginPath, closePath,stroke,fill等

充分理解例子

理论基础 -- canvas坐标

如下图,canvas中的坐标是从左上角开始,x轴沿着水平方向(按像素)向右延伸,y轴沿垂直方向向下延伸。

最左上角坐标为 (0,0) 的点为原点。

详细步骤 -- 使用HTML5 canvas API

检测浏览器支持情况

我们做两件事

  1. 我们用一段script判断浏览器支持情况。

    如果不支持可以将提示信息显示在特定的位置。

    如下图,我们用了一个id="support"的div来显示提示升级的信息。

  2. 我们在canvas中写入一段替代内容.

    如下图,如果不支持,canvas会显示替代内容。

浏览器支持

把IE调成IE7模式测试下不支持的情况:

 

利用canvas画一条对角线

对上面的例子做一些修改

在canvas中绘制一条对角线

根据上面的js代码,归纳出使用canvas API的重要流程

  1. 根据canvas ID值获取canvas对象访问权,接着定义一个context变量,调用canvas对象的getContext方法。
  2. 基于这个context执行动作(这里是画一条对角线)
  3. 通过context.stroke()完成线条的绘制。

Note

这里有一个坑。我原来将设置canvas长宽放在了style里面。如下图。

出现问题的原因:

canvas的width和height是画布的实际宽度和高度,绘制的图形在这个画布上面。

canvas的style的width和height是canvas在浏览器中被渲染的高度和宽度。

因此需要注意设置宽度时要在外面设置。

使用变换(transformation)画对角线

下面来看canvas上绘制图像的另外一种方式:使用变换(transformation)。

transformation是实现复杂canvas操作的最好方式(就单个上面绘制对角线来说看起来是更加复杂了点)

理解 变换(transformation):

把它当成是介于开发人员发出的指令和canvas显示结果之间的一个修正层 (modification layer)

注意 不管在开发中是否使用变换,修正层始终存在。

每个绘制操作的结果显示在canvas上之前都要经过修正层去修正。

虽然这么做增加了额外的复杂性,但却为绘制系统添加了更为强大的功能。

Note

不在代码中调用变换函数并不意味着可以提升canvas的性能。

canvas在执行的时候,变换会被呈现引擎隐式调用,这与开发人员是否直接调用无关。

可重用代码的一条重要建议:

一般绘制都应从原点开始,应用变换(缩放,平移,旋转等),然后不断修改代码直至达到希望的效果。

示例

这个代码的结果和上面是一模一样的。

大家注意这两种代码的差别:

对第二种方式, translate(70,140) 代表将原点移到 (70,140) 这个位置。

也就是说,接下来所有操作都是相对于 (70,140) 这个位置来操作的。

第一种情况是(70,140)à(140,70),

第二种情况是(0,0)à(70,140)à(70,-70)

第二种情况的(70, -70)是相对于新的原点(70,140)点来说的,相对于一开始的原点坐标是(70+70,-70+140),很容易看到这两种情况的结果是等价的,理解了吗?

大家体会一下。

我们归纳一下上面的操作:

  1. 根据canvas ID值获取canvas对象访问权,接着定义一个context变量,调用canvas对象的getContext方法。
  2. 保存尚未修改的context, 这样即使进行了绘制和变换操作,也可以恢复到初始状态(通过后面的restore函数)
  3. 通过translate来移动原点,这个上面已经解释过了。
  4. 基于移动过的context执行画线动作
  5. 通过context.stroke()完成线条的绘制。
  6. 最后,恢复context至原始状态,这样后续的canvas操作就不会被刚才的平移操作影响了。

画树

现在学习稍微复杂点的图形。

前面绘制的一条对角线算是一条简单路径。

实际上路径可以很复杂:多条线、曲线段、甚至是子路径。

如果想在canvas上绘制任意形状,那么你需要重点关注路径API

按照惯例,不论开始绘制何种图形,第一个需要调用的就是beginPath, 对于canvas来说,beginPath函数的最大用处是canvas需要据此来计算图形的内部和外部范围,以便完成后续的描边和填充。

路径会跟踪当前坐标,默认值是原点。

调用beginPath之后,就可以使用context的各种方法来绘制想要的形状了。

到目前为止已经使用了几个简单的context路径函数。

moveTo(x,y)

lineTo(x,y)

上面两个函数的区别是:moveTo就像是提起画笔,移动到新位置;

而lineTo告诉canvas用画笔从纸上的旧坐标画条直线到新坐标。

注意,不管调用的是哪一个,都不会真正画出图形,因为我们还没有调用stroke或者fill函数。

目前我们只是定义路径的位置,以便后面绘制时使用。

另外再介绍一个路径函数closePath, 这个函数和lineTo很像,唯一的差别是会将路径的起始坐标 自动作为 目标坐标。

clothPath还会通知canvas当前绘制的图形已经完全闭合或者形成了完全封闭的区域,这对将来的填充和描边都非常有用。

此时,可以在已有的路径中继续创建其他的子路径,或者随时调用beginPath重新绘制新路径并完全清除之前的所有路径。

1.绘制树冠

绘制树冠的函数

 

为了直观的显示图线的走势,我画了个从开始点到顶点的草图,如下

在canvas上画树的函数:

最终结果如下

下面我们对树冠做一些美化,在stroke之前添加如下代码

变成了更粗更平滑的棕色线条。

进一步美化,将闭合路径内部填充为绿色

 

注意,右边的边框也变细了。

当我们采用先描边后填充的方式,会填充一半的边框。

如果要不填充边框,需要采用先填充后描边的方式,如下。

2. 利用fillRect画树干(填充矩形区域)

我们先把translate的数值改一下,让出树干的位置。

context.translate(130,150);

通过fillRect(x, y, 宽, 高)来画出树干。

注意,这段要在context.restore();前面,否则画的位置就错了。

最终结果:

Note

fillRect(x,y,width,height)

总结

大家初步可以看到canvas的威力,可以不用借助第三方技术进行绘图。

当然目前画的东西还比较简单,下篇文章将会在这棵树的基础上加入其他元素和特殊效果,完成一幅雨水动画效果的林荫小道图。

好了,今天就到这里,欢迎大家多多评论,让下一篇文章更好:)

 http://javascript.ruanyifeng.com/htmlapi/canvas.html

目录

概述

Canvas API(画布)用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)。

使用前,首先需要新建一个canvas网页元素。

<canvas id="myCanvas" width="400" height="200">
  您的浏览器不支持canvas!
</canvas>

上面代码中,如果浏览器不支持这个API,则就会显示canvas标签中间的文字——“您的浏览器不支持canvas!”。

每个canvas元素都有一个对应的context对象(上下文对象),Canvas API定义在这个context对象上面,所以需要获取这个对象,方法是使用getContext方法。

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

if (canvas.getContext) {
  var ctx = canvas.getContext('2d');
}

上面代码中,getContext方法指定参数2d,表示该canvas对象用于生成2D图案(即平面图案)。如果参数是webgl,就表示用于生成3D图像(即立体图案),这部分实际上单独叫做WebGL API(本书不涉及)。

绘图方法

canvas画布提供了一个用来作图的平面空间,该空间的每个点都有自己的坐标,x表示横坐标,y表示竖坐标。原点(0, 0)位于图像左上角,x轴的正向是原点向右,y轴的正向是原点向下。

(1)绘制路径

beginPath方法表示开始绘制路径,moveTo(x, y)方法设置线段的起点,lineTo(x, y)方法设置线段的终点,stroke方法用来给透明的线段着色。

ctx.beginPath(); // 开始路径绘制
ctx.moveTo(20, 20); // 设置路径起点,坐标为(20,20)
ctx.lineTo(200, 20); // 绘制一条到(200,20)的直线
ctx.lineWidth = 1.0; // 设置线宽
ctx.strokeStyle = "#CC0000"; // 设置线的颜色
ctx.stroke(); // 进行线的着色,这时整条线才变得可见

moveto和lineto方法可以多次使用。最后,还可以使用closePath方法,自动绘制一条当前点到起点的直线,形成一个封闭图形,省却使用一次lineto方法。

(2)绘制矩形

fillRect(x, y, width, height)方法用来绘制矩形,它的四个参数分别为矩形左上角顶点的x坐标、y坐标,以及矩形的宽和高。fillStyle属性用来设置矩形的填充色。

ctx.fillStyle = 'yellow';
ctx.fillRect(50, 50, 200, 100); 

strokeRect方法与fillRect类似,用来绘制空心矩形。

ctx.strokeRect(10,10,200,100); 

clearRect方法用来清除某个矩形区域的内容。

ctx.clearRect(100,50,50,50);  

(3)绘制文本

fillText(string, x, y) 用来绘制文本,它的三个参数分别为文本内容、起点的x坐标、y坐标。使用之前,需用font设置字体、大小、样式(写法类似与CSS的font属性)。与此类似的还有strokeText方法,用来添加空心字。

// 设置字体
ctx.font = "Bold 20px Arial"; 
// 设置对齐方式
ctx.textAlign = "left";
// 设置填充颜色
ctx.fillStyle = "#008600"; 
// 设置字体内容,以及在画布上的位置
ctx.fillText("Hello!", 10, 50); 
// 绘制空心字
ctx.strokeText("Hello!", 10, 100); 

fillText方法不支持文本断行,即所有文本出现在一行内。所以,如果要生成多行文本,只有调用多次fillText方法。

(4)绘制圆形和扇形

arc方法用来绘制扇形。

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

arc方法的x和y参数是圆心坐标,radius是半径,startAngle和endAngle则是扇形的起始角度和终止角度(以弧度表示),anticlockwise表示做图时应该逆时针画(true)还是顺时针画(false)。

下面是如何绘制实心的圆形。

ctx.beginPath(); 
ctx.arc(60, 60, 50, 0, Math.PI*2, true); 
ctx.fillStyle = "#000000"; 
ctx.fill();

绘制空心圆形的例子。

ctx.beginPath(); 
ctx.arc(60, 60, 50, 0, Math.PI*2, true); 
ctx.lineWidth = 1.0; 
ctx.strokeStyle = "#000"; 
ctx.stroke();

(5)设置渐变色

createLinearGradient方法用来设置渐变色。

var myGradient = ctx.createLinearGradient(0, 0, 0, 160); 

myGradient.addColorStop(0, "#BABABA"); 

myGradient.addColorStop(1, "#636363");

createLinearGradient方法的参数是(x1, y1, x2, y2),其中x1和y1是起点坐标,x2和y2是终点坐标。通过不同的坐标值,可以生成从上至下、从左到右的渐变等等。

使用方法如下:

ctx.fillStyle = myGradient;
ctx.fillRect(10,10,200,100);

(6)设置阴影

一系列与阴影相关的方法,可以用来设置阴影。

ctx.shadowOffsetX = 10; // 设置水平位移
ctx.shadowOffsetY = 10; // 设置垂直位移
ctx.shadowBlur = 5; // 设置模糊度
ctx.shadowColor = "rgba(0,0,0,0.5)"; // 设置阴影颜色

ctx.fillStyle = "#CC0000"; 
ctx.fillRect(10,10,200,100);

图像处理方法

drawImage方法

canvas允许将图像文件插入画布,做法是读取图片后,使用drawImage方法在画布内进行重绘。

var img = new Image();
img.src = "image.png";
ctx.drawImage(img, 0, 0); // 设置对应的图像对象,以及它在画布上的位置

上面代码将一个PNG图像载入canvas。

由于图像的载入需要时间,drawImage方法只能在图像完全载入后才能调用,因此上面的代码需要改写。

var image = new Image(); 

image.onload = function() { 
  var canvas = document.createElement("canvas");
	canvas.width = image.width;
	canvas.height = image.height;
	canvas.getContext("2d").drawImage(image, 0, 0);

	return canvas;
} 

image.src = "image.png";

drawImage()方法接受三个参数,第一个参数是图像文件的DOM元素(即img标签),第二个和第三个参数是图像左上角在Canvas元素中的坐标,上例中的(0, 0)就表示将图像左上角放置在Canvas元素的左上角。

getImageData方法,putImageData方法

getImageData方法可以用来读取Canvas的内容,返回一个对象,包含了每个像素的信息。

var imageData = context.getImageData(0, 0, canvas.width, canvas.height);

imageData对象有一个data属性,它的值是一个一维数组。该数组的值,依次是每个像素的红、绿、蓝、alpha通道值,因此该数组的长度等于 图像的像素宽度 x 图像的像素高度 x 4,每个值的范围是0–255。这个数组不仅可读,而且可写,因此通过操作这个数组的值,就可以达到操作图像的目的。修改这个数组以后,使用putImageData方法将数组内容重新绘制在Canvas上。

context.putImageData(imageData, 0, 0);

toDataURL方法

对图像数据做出修改以后,可以使用toDataURL方法,将Canvas数据重新转化成一般的图像文件形式。

function convertCanvasToImage(canvas) {
  var image = new Image();
  image.src = canvas.toDataURL("image/png");
  return image;
}

上面的代码将Canvas数据,转化成PNG data URI。

save方法,restore方法

save方法用于保存上下文环境,restore方法用于恢复到上一次保存的上下文环境。

ctx.save(); 

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

ctx.fillStyle = "#CC0000";
ctx.fillRect(10,10,150,100);

ctx.restore(); 

ctx.fillStyle = "#000000";
ctx.fillRect(180,10,150,100); 

上面代码先用save方法,保存了当前设置,然后绘制了一个有阴影的矩形。接着,使用restore方法,恢复了保存前的设置,绘制了一个没有阴影的矩形。

动画

利用JavaScript,可以在canvas元素上很容易地产生动画效果。


var posX = 20,
    posY = 100;

setInterval(function() {
	context.fillStyle = "black";
    context.fillRect(0,0,canvas.width, canvas.height);

	posX += 1;
	posY += 0.25;

	context.beginPath();
	context.fillStyle = "white";

	context.arc(posX, posY, 10, 0, Math.PI*2, true); 
	context.closePath();
	context.fill();
}, 30);

上面代码会产生一个小圆点,每隔30毫秒就向右下方移动的效果。setInterval函数的一开始,之所以要将画布重新渲染黑色底色,是为了抹去上一步的小圆点。

通过设置圆心坐标,可以产生各种运动轨迹。

先上升后下降。


var vx = 10,
    vy = -10,
    gravity = 1;

setInterval(function() {
    posX += vx;
    posY += vy;
    vy += gravity;
	// ...
});

上面代码中,x坐标始终增大,表示持续向右运动。y坐标先变小,然后在重力作用下,不断增大,表示先上升后下降。

小球不断反弹后,逐步趋于静止。


var vx = 10,
    vy = -10,
    gravity = 1;

setInterval(function() {
    posX += vx;
    posY += vy;

	if (posY > canvas.height * 0.75) {
          vy *= -0.6;
          vx *= 0.75;
          posY = canvas.height * 0.75;
    }
	
    vy += gravity;
	// ...
});

上面代码表示,一旦小球的y坐标处于屏幕下方75%的位置,向x轴移动的速度变为原来的75%,而向y轴反弹上一次反弹高度的40%。

像素处理

通过getImageData方法和putImageData方法,可以处理每个像素,进而操作图像内容。

假定filter是一个处理像素的函数,那么整个对Canvas的处理流程,可以用下面的代码表示。

if (canvas.width > 0 && canvas.height > 0) {

	var imageData = context.getImageData(0, 0, canvas.width, canvas.height);

    filter(imageData);

    context.putImageData(imageData, 0, 0);

}

以下是几种常见的处理方法。

灰度效果

灰度图(grayscale)就是取红、绿、蓝三个像素值的算术平均值,这实际上将图像转成了黑白形式。假定d[i]是像素数组中一个象素的红色值,则d[i+1]为绿色值,d[i+2]为蓝色值,d[i+3]就是alpha通道值。转成灰度的算法,就是将红、绿、蓝三个值相加后除以3,再将结果写回数组。

grayscale = function (pixels) {

	var d = pixels.data;

    for (var i = 0; i < d.length; i += 4) {
      var r = d[i];
      var g = d[i + 1];
      var b = d[i + 2];
      d[i] = d[i + 1] = d[i + 2] = (r+g+b)/3;
    }

    return pixels;

};

复古效果

复古效果(sepia)则是将红、绿、蓝三个像素,分别取这三个值的某种加权平均值,使得图像有一种古旧的效果。

sepia = function (pixels) {

    var d = pixels.data;

    for (var i = 0; i < d.length; i += 4) {
      var r = d[i];
      var g = d[i + 1];
      var b = d[i + 2];
      d[i]     = (r * 0.393)+(g * 0.769)+(b * 0.189); // red
      d[i + 1] = (r * 0.349)+(g * 0.686)+(b * 0.168); // green
      d[i + 2] = (r * 0.272)+(g * 0.534)+(b * 0.131); // blue
    }

    return pixels;

};

红色蒙版效果

红色蒙版指的是,让图像呈现一种偏红的效果。算法是将红色通道设为红、绿、蓝三个值的平均值,而将绿色通道和蓝色通道都设为0。

red = function (pixels) {
	
    var d = pixels.data;

    for (var i = 0; i < d.length; i += 4) {
      var r = d[i];
      var g = d[i + 1];
      var b = d[i + 2];
      d[i] = (r+g+b)/3;        // 红色通道取平均值
      d[i + 1] = d[i + 2] = 0; // 绿色通道和蓝色通道都设为0
    }

    return pixels;

};

亮度效果

亮度效果(brightness)是指让图像变得更亮或更暗。算法将红色通道、绿色通道、蓝色通道,同时加上一个正值或负值。

brightness = function (pixels, delta) {

    var d = pixels.data;

    for (var i = 0; i < d.length; i += 4) {
          d[i] += delta;     // red
          d[i + 1] += delta; // green
          d[i + 2] += delta; // blue   
    }

	return pixels;

};

反转效果

反转效果(invert)是指图片呈现一种色彩颠倒的效果。算法为红、绿、蓝通道都取各自的相反值(255-原值)。

invert = function (pixels) {

	var d = pixels.data;

	for (var i = 0; i < d.length; i += 4) {
		d[i] = 255 - d[i];
		d[i+1] = 255 - d[i + 1];
		d[i+2] = 255 - d[i + 2];
	}

	return pixels;

};

推荐阅读