最近笔者在学习HTML5的新元素<canvas>
,会分享一些基础知识以及小例子,最终使用<canvas>
实现一个绘制简单图表(条形图、线图或者饼图)的js库,会更新一到两篇文章~
下面我们开始吧~
确认宽度和高度
我们首先应该指定<canvas>
标签即画布的宽度和高度属性,并在开始和闭合标签之间添加后备信息:
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<canvas id="canvas" width="500" height="500">Canvas is not supported.</canvas>
</body>
</html>
取得绘图上下文
调用canvas
的getContext()
方法,这个方法接收一个参数,即上下文的名字:
var canvas = document.getElementById("canvas");
if(canvas.getContext) {
var context = canvas.getContext("2d")
}
上述例子中,我们在调用getContext()
方法时,首先检测其是否存在,这是由于有的浏览器遇到HTML规范之外的标签时,也会创建一个DOM对象,比如我们在Chrome中尝试以下代码:
<ppp id="ppp"></ppp>
document.getElementById("ppp");
//<ppp id="ppp"></ppp>
因此即使当前浏览器不能兼容HTML规范中的canvas元素,同样会创建DOM对象,可其中却不存在getContext()
。
context
上下文对象中包含了绘图需要的一系列属性和方法,大家在阅读本文时,记得区分属性和方法,改变属性会影响到后续的绘图效果,而调用方法往往是一次性的。
绘制简单的2D图形
坐标原点
2D上下文的坐标默认开始于左上角,原点坐标为(0, 0),使用translate(x, y)
可以更改坐标原点。
填充
填充,就是用指定的样式,比如颜色、渐变或图像填充指定区域,对应的上下文属性为fillStyle
,默认值为#000000
, 比如:
context.fillStyle = "orange";
描边
描边就是在指定区域边缘画指定样式的线,比如:
context.strokeStyle = "grey";
绘制矩形
矩形是唯一一个可以在2D上下文中直接话的形状,其他的都需要绘制路径,可以使用3个方法直接绘制矩形。
- fillRect(x, y, width, height)
可以绘制一个由fillStyle
指定填充样式的矩形,比如:
context.translate(100, 100)
context.fillStyle = "#99cccc";
context.fillRect(-100, -50, 200, 100);
context.fillStyle="#3399cc";
context.fillRect(-60, -30, 120, 60);
- strokeRect(x, y, width, height)
可以绘制一个由strokeStyle
lineWidth
lineCap
lineJoin
等属性指定描边样式的矩形,比如:
context.strokeStyle = "#99cccc";
context.lineWidth = "50";
context.lineJoin = "bevel";
context.strokeRect(0, 0, 400, 200);
属性名 | 含义 | 取值 |
---|---|---|
lineCap | 线条末端的形状 |
butt 平头round 圆头square 方头 |
lineWidth | 线条宽度 | 整数 |
lineJoin | 线条相交的方式 |
round 圆交bevel 斜交mitter 斜接 |
- clearRect(x, y, width, height)
可以清楚画布上的指定区域,比如第一个例子中的两个矩形,我们将中间一小块清除:
context.translate(100, 100)
context.fillStyle = "#99cccc";
context.fillRect(-100, -50, 200, 100);
context.fillStyle="#3399cc";
context.clearRect(-60, -30, 120, 60);
绘制路径
使用路径我们可以绘制出比较复杂的图形,开始绘制前,首先执行:
context.beginPath();
结束绘制时,如果需要将笔点返回到当前子路径起始点,执行:
context.closePath();
如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作
以下我们列举了绘制路径的几个方法:
直线
lineTo(x, y)
,从当前游标至(x, y)画一条直线。
移动游标
moveTo(x, y)
将游标移动至(x, y),移动过程中不画线。比如:
context.beginPath();
context.moveTo(10, 10);
context.lineTo(50, 40);
context.moveTo(50, 50);
context.lineTo(100, 90);
context.stroke();
context.closePath();
弧线或圆形
- arc(x, y, radius, startAngle, endAngle, counterclockwise)
从startAngle到endAngle绘制一条以(x, y)为圆心,radius为半径的弧线,其中startAngle和endAngle用弧度表示,couterclockwise为false时,顺时针画弧线, 反之,逆时针画弧线。
var canvas = document.getElementById("canvas");
if(canvas.getContext) {
var context = canvas.getContext("2d");
context.beginPath();
context.arc(400, 400, 50, arcUnit()*30, arcUnit()*180, false);
context.stroke();
}
function arcUnit() {
return Math.PI/180;
}
- arcTo(startX, startY, endX, endY, radius)
arcTo()
方法将利用当前坐标、起点(startX,startY)和终点(endX,endY)这三个点所形成的夹角,绘制一段与夹角的两边相切并且半径为radius的圆上的弧线。弧线的起点就是当前坐标所在边与圆的切点,弧线的终点就是终点(endX,endY)所在边与圆的切点,并且绘制的弧线是两个切点之间长度最短的那个圆弧。此外,如果当前端点不是(startX,startY),arcTo()方法还将添加一条当前端点到(startX,startY)的直线线段。
如果大家还记得高中数学的话,我们应该可以猜到,使用这个方法画弧线大致有三种情况:
** 只有一种半径可以使弧线两端正好在起点和终点
** 如果半径过大,画不出弧线
** 如果半径较小,必定会有一条当前端点到起点的直线
我们举三个例子:
首先定义一些通用的
var context = canvas.getContext("2d");
var currentPoint = {
x: 0,
y: 0
};
var startPoint = {
x: 50,
y: 50
};
var endPoint = {
x: 100,
y: 0
};
然后绘制参考线
context.moveTo(currentPoint.x, currentPoint.y);
context.lineTo(startPoint.x, startPoint.y);
context.lineTo(endPoint.x, endPoint.y);
context.strokeStyle = "red";
context.stroke();
画第一条弧线
context.moveTo(currentPoint.x, currentPoint.y);
context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 80);
context.strokeStyle = "grey";
context.stroke();
context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 120);
context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 40);
曲线
- 二次贝塞尔曲线
quadraticCurveTo(cpX, cpY, x, y)
上述方法可以画一条从当前位置到(x, y), 以(cpX, cpY)为控制点的贝塞尔曲线 - 三次贝塞尔曲线
bezierCurveTo(cpX1, cpY1, cpX2, cpY2, x, y)
上述方法可以画一条从当前位置到(x, y), 以(cpX1, cpY1)和(cpX2, cpY2)为控制点的贝塞尔曲线。
二次贝塞尔与三次贝塞尔的绘制涉及到比较复杂的数学运算,笔者在此就忽略啦...
当然,大部分前端人士可能都跟笔者一样,只希望能画出一条优美的曲线,并不关心实现细节,那么一般认为什么样的曲线才算是优美的曲线呢:
- 对称的曲线
- 有一个适度的弧度
根据以上规则,我们写一个工具方法:
function drawCurvePath( ctx, start, end, curveness ) {
var cp = [
( start.x + end.x ) / 2 - ( start.y - end.y ) * curveness,
( start.y + end.y ) / 2 - ( end.x - start.x ) * curveness
];
ctx.moveTo( start.x, start.y );
ctx.quadraticCurveTo(
cp[ 0 ], cp[ 1 ],
end.x, end.y
);
ctx.stroke();
}
以上参考自用canvas绘制一个曲线动画——深入理解贝塞尔曲线,大家可以前往了解更深入的贝塞尔曲线画法。
矩形
使用rect(x, y, width, height)
可以绘制一个左上角坐标为(x, y),宽width,高height的矩形路径。
context.rect(300, 300, 100, 200);
context.stroke();
绘制文本
在绘制本文之前,如果有必要,我们首先应该指定context的几个属性, 比如:
context.font = "bold 14px Arial"; // 格式同css中指定字体样式
context.textAlign = "center"; // start end center left right
context.textBaseline = "middle"; // top hanging middle alphabetic ideographic bottom
fillText(text, x, y, maxWidth)
使用fillStyle
属性显示文本,strokeText(text, x, y, maxWidth)
使用strokeStyle
属性为文本描边。
使用measureText(text)
方法可以获得文本的宽度。如果我们并不清楚指定的宽度够不够显示当前字体设置下的一段文字,可以使用如下方法:
var fontSize = 50;
var maxWidth = 100;
context.font = "bold " + fontSize+"px Arial";
var text = "Hello World!";
while(context.measureText(text).width > maxWidth) {
fontSize--;
context.font = "bold " + fontSize+"px Arial";
}
context.fillText(text, 50, 50, maxWidth);
变换
旋转
rotate(angle)
context.rotate(Math.PI/4)
context.fillText(text, 50, 50, maxWidth);
缩放
scale(scaleX, scaleY)
context.scale(1.2, 1.2);
context.fillText(text, 50, 50, maxWidth);
context.scale(0.5, 0.5);
context.fillText(text, 50, 50, maxWidth);
移动坐标原点
translate(x, y)
假如我们要绘制一个对称图形,移动坐标原点将会大大简化对坐标的计算。
矩阵变换
使用transform(scaleX,skewX,skewY,scaleY,transX,transY)
可以进行矩阵变换,其实以上讲的三个方法本质上都在调用矩阵变换,从参数名中可以看出,它们分别表示X轴方向的缩放,Y轴方向的缩放,X轴方向的斜切,Y轴方向的斜切,X轴方向的偏移量,Y轴方向的偏移量。默认值分别为1 0 0 1 0 0。
我们现在使用transform()
重新定义一遍之前的三个方法:
rotate
function rotate (ctx, degree) {
var unit = Math.PI/180;
ctx.transform(Math.cos(degree*unit),Math.sin(degree*unit),-Math.sin(degree*unit),Math.cos(degree*unit),0,0)
}
scale
function scale (ctx, scale) {
ctx.transform(scale.x, 0, 0, scale.y, 0, 0);
}
translate
function translate (ctx, translate) {
ctx.transform(1, 0, 0, 1, translate.x, translate.y);
}
我们现在绘制一段文本,先平移(50, 50),然后缩放2倍,最后旋转30度。
context.fillText(text, 50, 50, maxWidth);
translate(context, {x: 50, y: 50});
scale(context, {x: 2, y: 2});
rotate(context, 30);
context.fillText(text, 50, 50, maxWidth);
每次执行
transform()
都是基于上一次的结果,并不是初始状态,很多时候我们想执行的是基于初始状态旋转、平移或缩放,而非上一次的状态,此时我们可以使用setTransfrom(scaleX,skewX,skewY,scaleY,transX,transY)
,此方法首先会重置变换矩阵,然后执行transform()
。更加详细的内容可以参考html5 Canvas画图教程26:用transform来实现位移,缩放,旋转等
绘制图像
参数 | 含义 |
---|---|
image | 要绘制的图像,可以是HTMLImageElement,也可以是canvas |
x1 | 源图像的x坐标 |
y1 | 源图像的y坐标 |
width1 | 源图像的宽度 |
height1 | 源图像的高度 |
x2 | 画布的x坐标 |
y2 | 画布的y坐标 |
width2 | 图像在画布上显示的宽度 |
height2 | 图像在画布上显示的高度 |
使用drawImage(image, x1, y1, width1, height1, x2, y2, width2, height2)
可以将图像的指定部分按照指定的大小绘制到画布指定的位置上。
参数 | 含义 |
---|---|
image | 要绘制的图像,可以是HTMLImageElement,也可以是canvas |
x1 | 源图像的x坐标 |
y1 | 源图像的y坐标 |
width1 | 源图像的宽度 |
height1 | 源图像的高度 |
x2 | 画布的x坐标 |
y2 | 画布的y坐标 |
width2 | 图像在画布上显示的宽度 |
height2 | 图像在画布上显示的高度 |
绘制阴影
如果要给形状或路径加上阴影,我们要在绘制前设置context
对象的以下属性:
属性 | 含义 |
---|---|
shadowColor | 阴影颜色 |
shadowOffsetX | x轴偏移量 |
shadowOffsetY | y轴偏移量 |
shadowBlur | 模糊的像素数 |
context.shadowColor = "grey";
context.shadowOffsetX = "20";
context.shadowBlur = "5";
context.fillText(text, 50, 50, maxWidth);
渐变
线性渐变
使用createLinearGradient(startX, startY, endX, endY)
来创建线性渐变,这个方法确认了渐变的起始和方向,然后我们通过addColorStop(position, color)
来添加渐变的颜色,position是0到1的数字。一下笔者画一个超级喜欢的条纹渐变:
var grad = context.createLinearGradient(50, 50, 200, 200)
grad.addColorStop(0, "grey");
grad.addColorStop(0.3, "grey");
grad.addColorStop(0.3, "red");
grad.addColorStop(0.5, "red");
grad.addColorStop(0.5, "orange");
grad.addColorStop(0.7, "orange");
context.fillStyle = grad;
context.fillRect(50, 50, 150, 150);
径向渐变
使用createRadialGradient(centerX1, centerY1, radius1, centerX2, centerY2, radius2)
创建径向渐变。
图像重复
使用createPattern(image, repeatType)
可以绘制重复的图像,用来填充或描边,第一个参数是要重复的HTMLImageElement或canvas或video,第二个参数表示如何重复该图像,可取repeat
repeat-x
repeat-y
no-repeat