Canvas的一点使用经验(2D上下文)

按需求在项目里画了一个Canvas动画,记录一些经验

关于Canvas

  1. Canvas可以说是HTML 5最受欢迎的部分了,除了基本绘图的2D上下文,还提供了3D上下文(名为WebGL)
  2. 2D上下文主要操作就是填充、描边;还可以加入文字和变换图形,添加阴影、渐变。每一帧都要把画布上的每一个像素画出来。
  3. WebGL不是W3C制定的标准,本质是OpenGL概念在JavaScript中的实现,学习地址

Canvas 2D上下文的一些主要API

  • beginPath
    1. canvas的画图思路:“通过绘制路径来绘制图形,路径是一系列点的集合”。类似PS里的钢笔工具。
    2. 绘制路径的时候,实际上会有一个路径列表在帮我们纪录当前所画的的子路径,而这整一个列表就是我们当前绘制的路径。
    3. CanvasRenderingContext2D.beginPath() 是 Canvas 2D API 通过清空子路径列表并开始一个新路径的方法。
  • closePath
    1. 将笔点返回到当前子路径起始点的方法,尝试从当前点到起始点绘制一条直线。如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。
    2. 注意,说的是“子路径”。
    3. 有些方法会自动闭合路径,比如fill()和clip()。
  • moveTo
    1. 将一个新的子路径的起始点移动到(x,y)坐标的方法。
    2. 可以理解为从画布上“提起画笔”并且移动到某个位置。

操作的一些Tips

  • 另一种清除+重置画布的方法:
    1. var graph = document.getElementById('graph')
    2. graph.width = graph.width
  • 实线和虚线的切换方式:setLineDash([])
    1. 数组放空就是实线。很有趣的切换方式。
    2. 数组不为空,则以数组里的长度顺序来循环展示虚线的表现,并以数组为最小单元进行循环(线条、空格这样的循环顺序)。
  • 绘制过程控制
    1. save和restore是很有用的,可以通过save和restore把当前画笔的设置推入/推出栈。
    2. beginPath()和closePath()是开始和结束一段路径的操作,后者会尝试直接在终点和起点之间建立联系。(fill和clip会自动尝试连接)
    3. stroke是描边,fill是填充(会自带closePath),区别与closePath,可以理解为closePath是把一个“框”闭合,而stroke可以把某个形状增加一个边(比如rect画一个矩形,用stroke就能描出他的四边)
    4. lineTo画线,但是没有画下可视的线,stroke才会把上面的线画出来(变成可视),所以可以通过组合使用lineTostrokefill来画出各种不规则的有填充色有边色的图形。
  • 建议把一些基础量全部写成变量,而且变量一定要好好命名,太长不好用,但是太短会很容易搞混。
  • TODO: 画网页动画的时候,折线(动画产生)我遇到了不精确的问题,我通过强制规定转折点的坐标来规避,但总觉得应该有更好的方式。

性能优化

离屏Canvas

  1. drawImage方法的第一个参数不仅可以接收Image对象,也可以接收另一个Canvas对象。而且,此时使用Canvas对象绘制的开销与使用Image对象的开销几乎完全一致。
  2. 而使用Image对象的时候,绘制同样的一块区域,如果使用尺寸相仿的一张图片做数据源,那么性能会比较好;而如果选择一张大图上的一部分做数据源,性能就会比较差,因为每一次绘制还会包含裁剪工作。
  3. 因此有的时候我们可以在一个未插入页面的Canvas中去绘制,然后在需要的帧里用drawImage来使用这个离屏Canvas对象。
  4. 可以像这样做:

    1
    2
    3
    4
    5
    6
    7
    var cache = document.createElement('canvas');
    // 离屏Canvas对象
    var cachectx = cache.getContext('2d')
    cachectx.width = width;
    cachectx.height = height;
    <!-- 然后在需要绘制的地方 -->
    ctx.drawImage(cachectx, x, y);

分层Canvas

  1. 也就是“在画布上叠加画布”
  2. 分层Canvas在几乎任何动画区域较大、动画动作较复杂的情形下都是非常有必要的。分层Canvas能够大大降低完全不必要的渲染性能开销。
  3. 能够在每层Canvas上保持不同的重绘频率已经是最大的好处了。
  4. 需要做的,仅仅是生成多个Canvas实例,把它们重叠放置,父容器设置relative,每个Canvas设置absolute并使用不同的z-index来实现有次序的堆叠。
  5. 堆叠在上方的(z-index意义上的)Canvas中的内容会覆盖住下方Canvas中的内容。因此在必要的时候对某些Canvas设置background: transparent
  6. 注意,分层的绝对定位要写内联样式,不然Canvas初始化之后就没法去设置了。

实践里的一些总结

0.5px和线条

  1. 网上本来有一个说法是0.5px问题说线条模糊是因为每个线条都是从中心向两侧扩展的
  2. 但是在显示的时候,不足一个像素的,会被补足,造成模糊
  3. 给出的解决方案是:ctx.translate(0.5, 0.5)
  4. 我在项目里,没有遇到这个问题,也可能是因为我本身为了处理清晰度问题,将Canvas做了缩放

整体缩放

  1. 针对的问题是:锯齿、文字模糊、插入的图片模糊,实测效果非常不错。
  2. Canvas本身有一个属性canvas.widthcanvas.height,这和canvas.style.widthcanvas.style.height不同的东西
  3. 前者是Canvas的大小(有默认值300px和150px),后者是浏览器渲染Canvas这个DOM的尺寸。同时利用它们,就可以实现缩放。
  4. 首先给其父容器一个宽、高,我们假定为wh
  5. 通过CSS设置canvas.style.width = 100%
  6. 我们选择一个缩放比tScale,然后设置canvas自己的属性canvas.widthcanvas.height,然后设置w * tScaleh * tScale
  7. 之后,在Canvas里作图的时候,设定的基础长度全部乘以缩放比tScale,推荐建一个cal函数去计算,这样写的时候还可以写原值,可读性更强。比如这样:

    1
    2
    3
    const cal = (scale) => {
    return ScreenInfo.screenWidth / 375 * scale * tScale
    };
  8. 那么,上面说的缩放到底应该缩放到什么程度呢?怎么选择tScale这个值?不小于设备本身的像素比就ok了,比如直接:tScale = window.devicePixelRatio

  9. 切记,缩放一定要把所有的一起缩放。
  10. Canvas的widthheight属性只能在一开始设定,之后只能获取。所以Canvas应该在需要的数据被获取到的时候去创建。然后,每次要重新绘制之前,就要去重置这些数据,将他们变成初始状态,这样就不会出现Canvas被上次的数据渲染的问题。

字体设置的坑

  1. 上面的解决方案,可以成功地让画布变得清晰,特别是一些引入的图片,也变得更加清晰和边缘平滑
  2. 然后遇到了一个字体的坑,Canvas可以通过ctx.font来设置字体,接受一个字符串的值,官方文档写的是ctx.font="12px serif"这里有两个坑
  3. 这里不接受字符串模版,可以接受变量,但是需要用字符串拼接(没错就是加号)
  4. 两个参数缺一不可,不写字体的话,字体大小的设置也不会生效。