Eloquent JavaScript 笔记 十六:Drawing on Canvas

来源:互联网 发布:sql select where and 编辑:程序博客网 时间:2024/06/04 19:09

在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>

原创粉丝点击