目 录 | |
---|---|
1 | canvas简介 |
2 | 基本用法 |
3 | 绘制基本图形 |
4 | 组合使用 |
1.canvas简介
canvas 是 HTML5 新增的元素,与JavaScript结合使用来绘制图形。例如:画图,合成照片,创建动画甚至实时视频处理与渲染。很多js图标插件都是基于canvas来实现的,比如charts.js、echats3。
- canvas 首先是由 Apple 引入的,用于 OS X Dashboard 和 Safari。
- Mozilla 程序从 Gecko 1.8 (Firefox 1.5) 开始支持 canvas。
- Internet Explorer 从IE9开始canvas ,更旧版本的IE可以引入 Google 的 Explorer Canvas 项目中的脚本来获得canvas支持。
- Chrome和Opera 9+ 也支持 canvas。
2.基本用法
2.1 canvas元素
<canvas id="myCanvas" width="150" height="150"></canvas>
canvas 标签只有两个属性—— width和height。当没有设置宽度和高度的时候,canvas会初始化宽度为300像素和高度为150像素。canvas可以使用CSS来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果CSS的尺寸与初始画布的比例不一致,canvas中的图像会被扭曲。
2.2 替换内容
<canvas id="currentTime" width="150" height="150">
<img src="images/currentTime.png" width="150" height="150" alt=""/>
</canvas>
可在canvas的标签内部定义替换内容。
当某些较老的浏览器不支持 canvas 时,浏览器会加载canvas标签内部的元素展示。
当浏览器支持 canvas 是,浏览器会忽略canvas标签包含的内容,正常渲染canvas。
2.3 渲染上下文(The rendering context)
canvas 元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,用来绘制和处理要展示的内容。
canvas起初是空白的。为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制。canvas 元素的 getContext() 的方法,就是是用来获得渲染上下文和它的绘画功能。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// 虽然参数2d会让人有想法,但是很遗憾目前html5不提供3d服务。
代码的第一行通过使用 document.getElementById() 方法来为 canvas 元素得到DOM对象。一旦有了元素对象,便可以通过使用它的getContext() 方法来访问绘画上下文。
3. 绘制基本图形
3.1 栅格
在开始画图之前,需要了解一下画布栅格(canvas grid)以及坐标空间。
如上图所示,canvas元素默认被网格所覆盖。通常网格中的一个单元相当于canvas元素中的一像素。
栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。
所以图中蓝色方形左上角的坐标为距离左边(Y轴)x像素,距离上边(X轴)y像素(坐标为(x,y))。
3.2 绘制矩形
不同于SVG,HTML中的元素canvas只支持一种原生的图形绘制:矩形。所有其他的图形的绘制都至少需要生成一条路径。不过很多的路径生成方法让复杂图形的绘制成为了可能。下面来看一下矩形的绘制。
canvas提供了三个矩形绘制相关的方法:
// 绘制一个填充的矩形
fillRect(x, y, width, height)
// 绘制一个矩形的边框
strokeRect(x, y, width, height)
// 清除指定矩形区域,让清除部分完全透明。
clearRect(x, y, width, height)
上面提供的方法之中每一个都包含了相同的参数。x与y指定了在canvas画布上所绘制的矩形的左上角(相对于原点)的坐标。width和height设置矩形的尺寸。
下面来看一个具体的例子。
<script>
var canvas = document.getElementById('cvs1');
var ctx = canvas.getContext('2d');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
// fillRect()绘制了一个边长为100px的黑色正方形
ctx.fillRect(25,25,100,100);
// clearRect()从正方形的中心开始擦除了一个60*60px的正方形
ctx.clearRect(45,45,60,60);
// strokeRect()在清除区域内生成一个50*50的正方形边框。
ctx.strokeRect(50,50,50,50);
}
</script>
该例子的输出如下图所示。
不同于下面所要介绍的路径函数(path function),以上的三个函数绘制之后会马上显现在canvas上,即时生效。
3.3 绘制路径
3.3.1 路径简介
图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。
- 首先,需要创建路径起始点。
- 然后使用画图命令去画出路径。
- 之后把路径封闭。
- 一旦路径生成,就能通过描边或填充路径区域来渲染图形。
以下是所要用到的函数:
-
beginPath()
新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
-
closePath()
闭合路径之后图形绘制命令又重新指向到上下文中。
-
stroke()
通过线条来绘制图形轮廓。
-
fill()
通过填充路径的内容区域生成实心的图形。
生成路径的第一步叫做beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。
第二步就是调用函数指定绘制路径。
第三,就是闭合路径closePath(),此步不是必需的。这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的,即当前点为开始点,该函数什么也不做。
而当调用fill()函数时,没有闭合的形状都会自动闭合,所以不需要调用closePath()函数。但是调用stroke()时不会自动闭合。
下面来通过路径绘制一个三角形。
3.3.2 绘制三角形
function draw2() {
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
// 新建路径
ctx.beginPath();
// 移动笔触
ctx.moveTo(75,50);
// 划一条直线至(x,y)点
ctx.lineTo(100,75);
ctx.lineTo(100,25);
// 填充
ctx.fill();
}
}
输出如下:
例子所用到的moveTo()函数和lineTo()函数,下面来分别介绍。
3.2.3 移动笔触
moveTo()函数是移动笔触的函数。
这个函数实际上并不能画出任何东西,仅仅是将绘制点移动至某个坐标。可以想象一下在纸上作业,一支笔的笔尖从一个点到另一个点的移动过程。
- moveTo(x, y)
将笔触移动到指定的坐标x以及y上。
当canvas初始化或者beginPath()调用后,通常会使用moveTo()函数设置起点。
也可以使用moveTo()绘制一些不连续的路径,比如下面这个例子。
function draw3() {
var canvas = document.getElementById('cvs3');
var ctx = canvas.getContext('2d');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // 绘制
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();
}
}
function draw4() {
var canvas = document.getElementById('cvs4');
var ctx = canvas.getContext('2d');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // 绘制
// 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();
}
}
左边是使用moveTo()的效果,右边是不使用moveTo()的效果,至于例子中的arc()函数会在下面介绍。
3.2.4 直线
绘制直线,用到的方法是lineTo()。
-
lineTo(x, y)
绘制一条从当前位置到指定x以及y位置的直线。
该方法有两个参数:x以及y,代表坐标系中直线结束的点。
开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,开始点也可以通过moveTo()函数改变。
下面的例子绘制两个三角形,一个是填充的,另一个是描边的。
function draw5() {
var canvas = document.getElementById('cvs5');
var ctx = canvas.getContext('2d');
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();
}
}
可以注意到填充与描边三角形步骤有所不同。
正如上面所提到的,因为路径使用填充(filled)时,路径自动闭合,使用描边(stroked)则不会闭合路径。
如果没有添加闭合路径closePath()到描述三角形函数中,则只绘制了两条线段,并不是一个完整的三角形。
例子输出如下:
3.2.5 圆弧
绘制圆弧或者圆,可以使用arc()函数和arcTo()函数,然而根据MDN的说法,arcTo()函数的实现不是很靠谱,所以这里不作详细介绍。
-
arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
-
arcTo(x1, y1, x2, y2, radius)
根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点。
arc()函数有五个参数:
- x,y为绘制圆弧所在圆上的圆心坐标。
- radius为半径。
- startAngle以及endAngle参数用弧度定义了开始以及结束的弧度。这些都是以x轴为基准。
- 参数anticlockwise 为一个布尔值。为true时,是逆时针方向,否则顺时针方向。
下面的例子比上面的要复杂一下,下面绘制了12个不同的角度以及填充的圆弧。
function draw6() {
var canvas = document.getElementById('cvs6');
var ctx = canvas.getContext('2d');
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 ? false : true; // 顺时针或逆时针
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i>1){
ctx.fill();
} else {
ctx.stroke();
}
}
}
}
}
上面两个for循环,生成圆弧的行列(x,y)坐标。每一段圆弧的开始都调用beginPath()。
x,y坐标是可变的。半径(radius)和开始角度(startAngle)都是固定的。结束角度(endAngle)在第一列开始时是180度(半圆)然后每列增加90度。最后一列形成一个完整的圆。
clockwise 语句作用于第一、三行是顺时针的圆弧,anticlockwise作用于二、四行为逆时针圆弧。if 语句让一、二行描边圆弧,下面两行填充路径。
3.2.6 贝塞尔(bezier)以及二次贝塞尔
贝塞尔曲线在之前的svg介绍中也有提及,下面来看看canvas中的贝塞尔曲线。
-
quadraticCurveTo(cp1x, cp1y, x, y)
绘制贝塞尔曲线,cp1x,cp1y为控制点,x,y为结束点。
-
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制二次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
下面的图能够很好的描述两者的关系,贝塞尔曲线有一个开始、结束点(蓝色)以及一个控制点(红色),而二次贝塞尔曲线使用两个控制点。
参数x、y在这两个方法中都是结束点坐标。cp1x,cp1y为坐标中的第一个控制点,cp2x,cp2y为坐标中的第二个控制点。
下面来看两个个简单的例子。
// 一次贝塞尔
function draw7() {
var canvas = document.getElementById('cvs7');
var ctx = canvas.getContext('2d');
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 draw8() {
var canvas = document.getElementById('cvs8');
var ctx = canvas.getContext('2d');
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();
}
}
使用一次以及二次贝塞尔曲线是有一定的难度的,因为不同于像Adobe Illustrators这样的矢量软件,canvas所绘制的曲线没有直接的视觉反馈。这让绘制复杂的图形十分的困难。即使这样,只要有时间和耐心,很多复杂的图形都可以绘制出来。
4.组合使用
目前为止,每一个例子中的每个图形都只用到一种类型的函数,所以下面来看看将这些函数组合绘图的效果。
function draw9() {
var canvas = document.getElementById('cvs9');
var ctx = canvas.getContext('2d');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(150, 0);
ctx.lineTo(75, 75);
ctx.lineTo(150, 150);
ctx.lineTo(225, 75);
ctx.lineTo(150, 0);
ctx.stroke();
ctx.closePath();
ctx.strokeRect(75,0, 150, 150);
ctx.beginPath();
ctx.arc(150, 75, 52, Math.PI / 2, Math.PI * 3 / 2);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(150,23);
ctx.bezierCurveTo(190,30,190,70,150,75);
ctx.bezierCurveTo(110,82,110,122,150,127);
ctx.bezierCurveTo(220,127,220,23,150,23);
ctx.fill();
ctx.beginPath();
ctx.arc(150, 49, 5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(150, 101, 5, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
}
上面例子中的fillStyle属性可以用来设置fill填充颜色,颜色与样式的内容会在下一章介绍。
function draw10() {
var canvas = document.getElementById('cvs10');
var ctx = canvas.getContext('2d');
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();
}
这个例子是MDN上的一个例子,这个例子看着复杂,只要耐下性子看看,其实很容易理解,这里就不详细说明。
重要的一点是绘制上下文中使用到了fillStyle属性,以及封装函数(例子中的 roundedRect())。
fillStyle属性会在下次介绍,而使用封装函数对于减少代码量以及复杂度十分有用,在canvas的使用中,有效的封装函数才是重中之重。