Eloquent JavaScript 笔记 十六:Drawing on Canvas

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值