canvas

# canvas

参考教程:https://www.hangge.com/blog/cache/search.php?key=canvas&page=1

  • 只有两个属性:width、height
  • 如果你绘制出来的图像是扭曲的, 尝试用width和height属性为明确规定宽高,而不是使用CSS。

[TOC]

# 一、基本认识

# 1.1 不支持的浏览器可替换内容

<canvas id="stockGraph" width="150" height="150">
  current stock price: $3.15 +0.15
</canvas>

<canvas id="clock" width="150" height="150">
  <img src="images/clock.png" width="150" height="150" alt=""/>
</canvas>
1
2
3
4
5
6
7

# 1.1.2 检查支持性

通过简单的测试getContext()方法的存在,脚本可以检查编程支持性。

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}
1
2
3
4
5
6
7
8

# 1.2 与SVG的区别

  • Canvas
    • 依赖分辨率
    • 不支持事件绑定
    • 最合适网页游戏
  • SVG
    • 不依赖分辨率
    • 支持事件绑定
    • 大型渲染区域的程序(例如百度地图)
    • 不能用来实现网页游戏

# 二、基本绘制

# 2.1 绘制矩形

  • fillRect(x, y, width, height)
    • 绘制一个填充的矩形
  • strokeRect(x, y, width, height)
    • 绘制一个矩形的边框
  • clearRect(x, y, width, height)
    • 清除指定矩形区域,让清除部分完全透明。

# 2.2 绘制路径

  • beginPath()
    • 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
  • closePath()
    • 闭合路径之后图形绘制命令又重新指向到上下文中。
  • stroke()
    • 通过线条来绘制图形轮廓。
    • 调用stroke()时不会自动闭合。
  • fill()
    • 通过填充路径的内容区域生成实心的图形。
    • 调用fill()函数时,所有没有闭合的形状都会自动闭合,不需要调用closePath()函数。

# 2.3 圆弧

  • 绘制圆弧或者圆,我们使用arc()方法。

  • arcTo()亦可,但并不可靠。

  • arc(x, y, radius, startAngle, endAngle, anticlockwise)

    • 画一个以(x,y)为圆心的以radius为半径的圆弧(圆)
    • 从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
      • sAngle 起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。eAngle 结束角,以弧度计。
      • anticlockwise可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。
  • 注意:arc()函数中的角度单位是弧度,不是度数。

    • 角度与弧度的js表达式:radians=(Math.PI/180)*degrees。

# 2.4 插入图片

  • 等图片加载完,再执行canvas操作
    • 图片预加载:在onload中调用方法
// oImg: 当前图片
// x,y: 坐标
// w,h: 宽高
drawImage(oImg,x,y,w,h)
1
2
3
4

# 2.5 设置背景

createPattern(oImg,平铺方式)
// repeat、repeat-x、repeat-y、no-repeat
1
2

# 2.6 渐变色

// 添加渐变线
let grd1 = ctx.createLinearGradient(0, 300, 0, 0);
// 添加颜色断点
grd1.addColorStop(0, "rgba(10, 102, 202, 1)");
grd1.addColorStop(0.25, "rgba(10, 102, 202, 1)");
grd1.addColorStop(0.5, "rgba(10, 102, 202, 1)");
grd1.addColorStop(0.75, "rgba(33, 150, 243, 1)");
grd1.addColorStop(1, "rgba(33, 150, 243, 1)");
1
2
3
4
5
6
7
8

# 2.7 贝塞尔曲线

参考教程:https://www.jianshu.com/p/18a495956b15

# 2.8 save 与 restore

参考教程:https://juejin.im/post/5d1c577f6fb9a07ecd3d78ae

CanvasRenderingContext2D渲染环境包含了多种绘图的样式状态(属性有线的样式、填充样式、阴影样式、文本样式)

# 三、小案例

# 3.1 鼠标绘制

<script>
    window.onload = function () {
    var oCan = document.getElementById('can');
    var oM = oCan.getContext('2d'); //如果是3d绘图则是 wdbg1  ,但是会存在兼容问题
    //开始绘制

    function draw() {
        oCan.onmousedown = function (ev) {
            var ev = ev || window.event;
            oM.moveTo(ev.clientX - oCan.offsetLeft, ev.clientY - oCan.offsetTop);
            document.onmousemove = function (ev) {
                var ev = ev || window.event;
                oM.lineTo(ev.clientX - oCan.offsetLeft, ev.clientY - oCan.offsetTop);
                oM.stroke();;
            };
            document.onmouseup = function () {
                document.onmousemove = document.onmouseup = null;
            };
        };

    }
    draw();

}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
    <!-- 这里有个小问题,如果不是内联样式的话,则样式是按照默认的宽300px,高150px的比例写的。-->
    <!-- 比如这里写在外部的话就会变成高是宽的两倍  -->
    <canvas id="can" width="400" height="400">
        <!-- 如果不支持的话就会显示这里面的内容 -->
        <span>该浏览器不支持canvas</span>
    </canvas>
</body>
1
2
3
4
5
6
7
8

# 3.2 方块移动

window.onload = function () {
    var oCan = document.getElementById('can');
    var oR = oCan.getContext('2d');
    var num = 0; //用来规定方块的起始位置
    var come = 1; //记录方块移动的方向
    //规定填充的颜色
    oR.fillStyle = 'yellow';
    //规定边框绘制的颜色
    oR.strokeStyle = 'red';
    //规定边框的宽度
    oR.lineWidth = 10;
    //开个定时器让方块动起来
    setInterval(function () {
        //避免上一个绘制的矩形还留在画布上
        //清除整个画布(起始点X,起始点Y,宽,高)
        oR.clearRect(0, 0, oCan.width, oCan.height);
        //绘制矩形(起始点X,起始点Y,宽,高)
        oR.strokeRect(num, num, 100, 100);
        //绘制矩形(起始点X,起始点Y,宽,高)
        oR.fillRect(num, num, 100, 100);
        if(num<300&&come==1){
            num++;
        }else{
            come=0;
        };
        if(num>0 && come==0){
            num--;
        }else{
            come=1;
        }
    }, 5);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 3.3 时钟

window.onload = function () {
    var oCan = document.getElementById('can');
    var oCir = oCan.getContext('2d');

    function toDraw() {
        //钟表的中心位置
        var x = 200;
        var y = 200;
        //钟表的半径
        var r = 150;

        oCir.clearRect(0, 0, oCan.width, oCan.height);

        //获取当前时间
        var oDate = new Date();
        var oHour = oDate.getHours();
        var oMin = oDate.getMinutes();
        var oSec = oDate.getSeconds();

        //绘制分的刻度
        //旋转的角度
        //-90是为了让起始点从三点方向回到十二点方向
        var oHourValue = (-90 + oHour * 30 + oMin / 2) * Math.PI / 180;
        var oMinValue = (-90 + oMin * 6) * Math.PI / 180;
        var oSecValue = (-90 + oSec * 6) * Math.PI / 180;

        //起始一条路径,或重置当前路径
        oCir.beginPath();

        for (var i = 0; i < 60; i++) {
            oCir.moveTo(x, y);
            //绘制圆(起始点X,起始点Y,半径,起始弧度,结束弧度,旋转方向)
            //弧度=角度*Math.PI/180
            oCir.arc(x, y, r, 6 * i * Math.PI / 180, 6 * (i + 1) * Math.PI / 180, false);
        }
        //创建从当前点回到起始点的路径
        oCir.closePath();
        oCir.stroke();

        //画一个白色的圆去覆盖过长的分刻度
        oCir.fillStyle = 'white';
        oCir.beginPath();
        oCir.moveTo(x, y);
        oCir.arc(x, y, r * 19 / 20, 0, 360 * (i + 1) * Math.PI / 180, false);
        oCir.closePath();
        oCir.fill();

        //绘制时的刻度
        oCir.lineWidth = 3;
        oCir.beginPath();
        for (var i = 0; i < 12; i++) {
            oCir.moveTo(x, y);
            oCir.arc(x, y, r, 30 * i * Math.PI / 180, 30 * (i + 1) * Math.PI / 180, false);
        }
        oCir.closePath();
        oCir.stroke();

        //画一个白色的圆去覆盖过长的时刻度
        oCir.fillStyle = 'white';
        oCir.beginPath();
        oCir.moveTo(x, y);
        oCir.arc(x, y, r * 18 / 20, 0, 360 * (i + 1) * Math.PI / 180, false);
        oCir.closePath();
        oCir.fill();

        //画时针
        oCir.lineWidth = 5;
        oCir.beginPath();
        oCir.moveTo(x, y);
        oCir.arc(x, y, r * 10 / 20, oHourValue, oHourValue, false);
        oCir.closePath();
        oCir.stroke();

        //画分针
        oCir.lineWidth = 3;
        oCir.beginPath();
        oCir.moveTo(x, y);
        oCir.arc(x, y, r * 14 / 20, oMinValue, oMinValue, false);
        oCir.closePath();
        oCir.stroke();

        //画秒针
        oCir.lineWidth = 1;
        oCir.beginPath();
        oCir.moveTo(x, y);
        oCir.arc(x, y, r * 19 / 20, oSecValue, oSecValue, false);
        oCir.closePath();
        oCir.stroke();
    }
    //一秒走一次
    setInterval(toDraw,1000);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 3.4 方块旋转放大

window.onload = function () {
    var oCan = document.getElementById('can');
    var oR = oCan.getContext('2d');
    var rotateNum = 0;
    var scaleNum = 0;
    var value = 0;
    setInterval(toRotate, 30);

    function toRotate() {
        rotateNum++;
        if (scaleNum == 100) {
            value = -1;
        } else if (scaleNum == 0) {
            value = 1;
        }
        scaleNum += value;
        oR.clearRect(0, 0, oCan.width, oCan.height);
        //保存当前环境的状态
        oR.save();
        //重新映射画布上的 (0,0) 位置
        oR.translate(200, 200);
        //弧度 = 角度*Math.PI/180
        oR.rotate(rotateNum * Math.PI / 100);
        //fillRect应该写在rotate后面,不然rotate不起作用
        oR.scale(scaleNum / 50, scaleNum / 50); 
        //让旋转点从左上角改成中心
        //这句话应该写在scale后面,不然scale会以左上角为基点放大缩小
        oR.translate(-50, -50);
        oR.fillRect(0, 0, 100, 100);
        oR.restore(); //返回之前保存过的路径状态和属性
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 四、性能优化

参考教程:https://www.cnblogs.com/axes/p/3567364.html?utm_source=tuicool&utm_medium=referral%E3%80%82 (opens new window)

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas

# 4.1 在离屏canvas上预渲染相似的图形或重复的对象

如果你发现你的在每一帧里有好多复杂的画图运算,请考虑创建一个离屏canvas,将图像在这个画布上画一次(或者每当图像改变的时候画一次),然后在每帧上画出视线以外的这个画布。

建立两个 canvas 标签,大小一致,一个正常显示,一个隐藏(缓存用的,不插入dom中),先将结果draw缓存用的canvas上下文中,因为游离canvas不会造成ui的渲染,所以它不会展现出来,再把缓存的内容整个裁剪再 draw 到正常显示用的 canvas 上,这样能优化不少。

# 4.2 避免浮点数的坐标点,用整数取而代之

当你画一个没有整数坐标点的对象时会发生子像素渲染。

浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证在你调用drawImage()函数时,用Math.floor()函数对所有的坐标点取整。

# 4.3 不要在用drawImage时缩放图像

应该在离屏canvas中缓存图片的不同尺寸。

# 4.4 使用多层画布去画一个复杂的场景

你可能会发现,你有些元素不断地改变或者移动,而其它的元素,例如外观,永远不变。这种情况的一种优化是去用多个画布元素去创建不同层次。

例如,你可以在最顶层创建一个外观层,而且仅仅在用户输入的时候被画出。你可以创建一个游戏层,在上面会有不断更新的元素和一个背景层,给那些较少更新的元素。

# 4.5 用CSS设置大的背景图

如果像大多数游戏那样,你有一张静态的背景图,用一个静态的`` (opens new window)元素,结合background (opens new window) 特性,以及将它置于画布元素之后。这么做可以避免在每一帧在画布上绘制大图。

# 4.6 用CSS transforms特性缩放画布

CSS transforms (opens new window) 特性由于调用GPU,因此更快捷。最好的情况是,不要将小画布放大,而是去将大画布缩小。

# 4.7 关闭透明度

如果你的游戏使用画布而且不需要透明,当使用 HTMLCanvasElement.getContext() 创建一个绘图上下文时把 alpha 选项设置为 false 。这个选项可以帮助浏览器进行内部优化。

let ctx = canvas.getContext('2d', { alpha: false });
1

# 4.8 避免不必要的画布状态改变

有动画,请使用window.requestAnimationFrame() (opens new window) 而非window.setInterval()

# 五、异步与同步

# 5.1 toBlob-异步

# 5.2 toDataURL-同步