在HTML中绘图有三种方式:
1. 使用DOM element,定制它的样式;
2. 使用SVG;
3. 使用canvas。
1. SVG
svg是特殊形式的xml文档,可以直接插入HTML文档中,当然,也可以单独写一个svg文件,作为<img>的src引入。看个例子:
<p>Normal HTML here.</p>
<svg xmlns="http://www.w3.org/2000/svg">
<circle r="50" cx="50" cy="50" fill="red"/>
<rect x="120" y="5" width="90" height="90" stroke="blue" fill="none"/>
</svg>
在HTML中嵌入svg,实际上是创建了一个特殊的element,如:<circle>, <rect>。我们也可以用 js 管理/修改 这些element,如:
var circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");
2. The Canvas Element
<p>Before canvas.</p>
<canvas width="120" height="60"></canvas>
<p>After canvas.</p>
<script>
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
context.fillStyle = "red";
context.fillRect(10, 10, 100, 50);
</script>
canvas是一种HTML element,它所拥有的属性和普通element差不多,例如:width、height等。
若要在canvas上绘图,首先需要获取context对象,所有的绘图函数都是context的属性。context有两种类型:2d 和 webgl。WebGL 提供3D绘图接口,它使用OpenGL接口。本章只讨论2d context。
3. Filling and Stroking
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
cx.strokeRect(135, 5, 50, 50);
</script>
fill 绘制实心的图形,stroke 绘制空心的图形。
相关方法:fillRect() , fillStyle(), strokeRect(), strokeStyle(), lineWidth 等等。
如果canvas没有指定width和height,默认大小是 300 x 150 。
4. Paths
path是一组线的集合。虽说是集合,但它不能存储在一个数组中,只能用一组函数临时生成。例如:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
for (var y = 10; y < 100; y += 10) {
cx.moveTo(10, y);
cx.lineTo(90, y);
}
cx.stroke();
</script>
如果一个path是封闭的,它可以被fill。如果path不是封闭的,fill时会自动添加一条线把path的起点和终点连接起来。
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(50, 10);
cx.lineTo(10, 70);
cx.lineTo(90, 70);
cx.fill();
</script>
也可以调用closePath() 方法显式的封闭一个path,这个函数会真的在path中添加一条线,也就是说,调用cx.stroke() 能把它画出来。而对于上面的代码,没有调用 closePath(), 那么,调用 cx.stroke() 就不会画出最后那条封闭线。
5. Curves
画曲线主要有三种方法:二次曲线、贝塞尔曲线、弧线
要真正理解曲线的形状和参数的关系,需要一些数学功底,这个我不懂,暂且放一放。分别看看例子。
二次曲线:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control=(60,10) goal=(90,90)
cx.quadraticCurveTo(60, 10, 90, 90);
cx.lineTo(60, 10);
cx.closePath();
cx.stroke();
</script>
贝塞尔曲线:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control1=(10,10) control2=(90,10) goal=(50,90)
cx.bezierCurveTo(10, 10, 90, 10, 50, 90);
cx.lineTo(90, 10);
cx.lineTo(10, 10);
cx.closePath();
cx.stroke();
</script>
弧线:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=20
cx.arcTo(90, 10, 90, 90, 20);
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=80
cx.arcTo(90, 10, 90, 90, 80);
cx.stroke();
</script>
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
// center=(50,50) radius=40 angle=0 to 7
cx.arc(50, 50, 40, 0, 7);
// center=(150,50) radius=40 angle=0 to ½π
cx.arc(150, 50, 40, 0, 0.5 * Math.PI);
cx.stroke();
</script>
注意,arcTo() 和 arc() 的参数不同。
6. Drawing a Pie Chart
用调查问卷的数据,画一张饼图。
数据:
var results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
{name: "No comment", count: 175, color: "silver"}
];
算法:
每一块饼的弧度 = count / total * 2π
<canvas width="200" height="200"></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var total = results.reduce(function(sum, choice) {
return sum + choice.count;
}, 0);
// Start at the top
var currentAngle = -0.5 * Math.PI;
results.forEach(function(result) {
var sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
// center=100,100, radius=100
// from current angle, clockwise by slice's angle
cx.arc(100, 100, 100,
currentAngle, currentAngle + sliceAngle);
currentAngle += sliceAngle;
cx.lineTo(100, 100);
cx.fillStyle = result.color;
cx.fill();
});
</script>
7. Text
如何绘制文字?fillText() , strokeText() 。
先看一个简单例子:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.font = "28px Georgia";
cx.fillStyle = "fuchsia";
cx.fillText("I can draw text, too!", 10, 50);
</script>
8. Images
drawImage() 用来在canvas上绘制位图。drawImage() 的源,可以是<img>,或其它<canvas>,源element在DOM中不必显示。如:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
img.src = "img/hat.png";
img.addEventListener("load", function() {
for (var x = 10; x < 200; x += 30)
cx.drawImage(img, x, 10);
});
</script>
默认,drawImage() 按图片原始大小绘制。
drawImage() 的第2、3、4、5个参数指定源图片的区域,第6、7、8、9个参数指定canvas上的区域。
我们可以把一串图片放在一张图上,通过绘制图片的指定区域,实现动画效果。类似于使用css属性background-position实现动画。
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 24, spriteH = 30;
img.addEventListener("load", function() {
var cycle = 0;
setInterval(function() {
cx.clearRect(0, 0, spriteW, spriteH);
cx.drawImage(img,
// source rectangle
cycle * spriteW, 0, spriteW, spriteH,
// destination rectangle
0, 0, spriteW, spriteH);
cycle = (cycle + 1) % 8;
}, 120);
});
</script>
说明:
1. 一个小人儿的宽度是24,高度是30。
2. 在<img> 的 "load" 事件中绘制动画。
3. 每120ms绘制一幅图。
4. 绘制新的图片时,需要用 clearRect() 清除上一幅图,否则会叠加。
5. 只用了前8幅图,后面两个有其他用途,下面会讲。
9. Transformation
上面的动画中,小人儿从左向右跑动,如何让它从右向左跑呢? 可以做一组反转的图片,也可以使用canvas的变换函数。
canvas 有一组图形变换函数:scale(), rotate(), translate().
9.1. scale
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.scale(3, .5);
cx.beginPath();
cx.arc(50, 50, 40, 0, 7);
cx.lineWidth = 3;
cx.stroke();
</script>
scale() 会影响后面所有绘图行为。上面的代码画一个圆,scale() 把它的宽度x3,高度 /2 。
如果scale() 的参数是负值,会把图形沿坐标轴翻转,也就是把图形的x、y坐标乘以 -1。
例如,把上面的 cx.scale(3, 0.5) 改成 cx.scale(-3, 0.5) ,相当于把后面的arc() 的x坐标变成 (-50, 50)。看不见了。
9.2. transformations stack
调用多个变换函数会产生叠加效果,而且,叠加的顺序会影响绘图的结果。看下图:
左图先做 translate,后rotate。右图先rotate,后translate,最终得到的坐标系原点不同,从而导致以后的所有绘图行为的坐标都不一样。
9.3. 翻转图片
function flipHorizontally(context, around) {
context.translate(around, 0);
context.scale(-1, 1);
context.translate(-around, 0);
}
通过三次transform,可以翻转上面小人儿的奔跑方向。
原理:
第一张绿色三角形是原始图片。
第二张做了translate。
第三张做 scale(-1, 1) 镜像。
第四张translate,回到原始位置。
为什么要有around这个参数呢? 看下面翻转小人儿的代码:
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 24, spriteH = 30;
img.addEventListener("load", function() {
flipHorizontally(cx, 100 + spriteW / 2);
cx.drawImage(img, 0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH);
});
</script>
这个around的参数,就是原始图片中心点的x坐标。(还是没想明白 。。。)
10. Storing and Clearing Transformations
变换函数会影响后面所有的绘图行为,有时候我们需要清除它的影响,或者,在循环中反复使用一组transformations。
为此,canvas 提供了两个函数:context.save(), context.restore()。 实际上,它使用了一个堆栈,用于保存当前所有的context配置,不仅限于transformations。save 相当于push,restore相当于pop。
看一个例子,在循环中绘制如下图形(树枝?树根?):
<canvas width="600" height="300"></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
function branch(length, angle, scale) {
cx.fillRect(0, 0, 1, length);
if (length < 8) return;
cx.save();
cx.translate(0, length);
cx.rotate(-angle);
branch(length * scale, angle, scale);
cx.rotate(2 * angle);
branch(length * scale, angle, scale);
cx.restore();
}
cx.translate(300, 0);
branch(60, 0.5, 0.8);
</script>
注意,每一次循环都需要save和restore,否则,在循环中多次transform,变换会一次次叠加,鬼才知道会变换成什么样子。
11. Back to The Game
把上一章的DOMDisplay改成CanvasDisplay,使用canvas显示场景和sprites。
相当长的代码,留待以后再看吧。
12. Choosing a Graphics Interface
三种方式:
DOM: 简单,文档结构清晰,可以自动布局。
SVG: 作为矢量图
canvas: 大量小元素的绘制、刷新。适合游戏?
13. Exercise: Shapes
绘制梯形:
var cx = document.querySelector("canvas").getContext("2d");
function parallelogram(x, y) {
cx.beginPath();
cx.moveTo(x, y);
cx.lineTo(x + 50, y);
cx.lineTo(x + 70, y + 50);
cx.lineTo(x - 20, y + 50);
cx.closePath();
cx.stroke();
}
parallelogram(30, 30);
绘制diamond:
function diamond(x, y) {
cx.translate(x + 30, y + 30);
cx.rotate(Math.PI / 4);
cx.fillStyle = "red";
cx.fillRect(-30, -30, 60, 60);
cx.resetTransform();
}
diamond(140, 30);
注意,一定先做 transform (translate, rotate) ,真正的绘图函数写在后面。 resetTransform() 会清除掉所以的transform配置。
绘制折线:
function zigzag(x, y) {
cx.beginPath();
cx.moveTo(x, y);
for (var i = 0; i < 8; i++) {
cx.lineTo(x + 80, y + i * 8 + 4);
cx.lineTo(x, y + i * 8 + 8);
}
cx.stroke();
}
zigzag(240, 20);
绘制螺旋线:
function spiral(x, y) {
var radius = 50, xCenter = x + radius, yCenter = y + radius;
cx.beginPath();
cx.moveTo(xCenter, yCenter);
for (var i = 0; i < 300; i++) {
var angle = i * Math.PI / 30;
var dist = radius * i / 300;
cx.lineTo(xCenter + Math.cos(angle) * dist,
yCenter + Math.sin(angle) * dist);
}
cx.stroke();
}
spiral(340, 20);
绘制星星:
function star(x, y) {
var radius = 50, xCenter = x + radius, yCenter = y + radius;
cx.beginPath();
cx.moveTo(xCenter + radius, yCenter);
for (var i = 1; i <= 8; i++) {
var angle = i * Math.PI / 4;
cx.quadraticCurveTo(xCenter, yCenter,
xCenter + Math.cos(angle) * radius,
yCenter + Math.sin(angle) * radius);
}
cx.fillStyle = "gold";
cx.fill();
}
star(440, 20);
数学没学好,理解起来很困难,不仔细看它了。
14. Exercise: The Pie Chart
给前面画的饼图加上文字:
<script>
var results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
{name: "No comment", count: 175, color: "silver"}
];
var cx = document.querySelector("canvas").getContext("2d");
var total = results.reduce(function(sum, choice) {
return sum + choice.count;
}, 0);
var currentAngle = -0.5 * Math.PI;
var centerX = 300, centerY = 150;
results.forEach(function(result) {
var sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
cx.arc(centerX, centerY, 100,
currentAngle, currentAngle + sliceAngle);
var middleAngle = currentAngle + 0.5 * sliceAngle;
var textX = Math.cos(middleAngle) * 120 + centerX;
var textY = Math.sin(middleAngle) * 120 + centerY;
cx.textBaseLine = "middle";
if (Math.cos(middleAngle) > 0)
cx.textAlign = "left";
else
cx.textAlign = "right";
cx.font = "15px sans-serif";
cx.fillStyle = "black";
cx.fillText(result.name, textX, textY);
currentAngle += sliceAngle;
cx.lineTo(centerX, centerY);
cx.fillStyle = result.color;
cx.fill();
});
</script>
15. Exercise: A Bouncing Ball
<script>
var cx = document.querySelector("canvas").getContext("2d");
var lastTime = null;
function frame(time) {
if (lastTime != null)
updateAnimation(Math.min(100, time - lastTime) / 1000);
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
var x = 100, y = 300;
var radius = 10;
var speedX = 100, speedY = 60;
function updateAnimation(step) {
cx.clearRect(0, 0, 400, 400);
cx.strokeStyle = "blue";
cx.lineWidth = 4;
cx.strokeRect(25, 25, 350, 350);
x += step * speedX;
y += step * speedY;
if (x < 25 + radius || x > 375 - radius)
speedX = -speedX;
if (y < 25 + radius || y > 375 - radius)
speedY = -speedY;
cx.fillStyle = "red";
cx.beginPath();
cx.arc(x, y, radius, 0, 7);
cx.fill();
}
</script>