你应该知道的Canvas使用及常见问题

7,179 阅读8分钟

前言

在具体项目开发时,我们经常会使用canvas绘制图表、丰富的图形、动画交互等应用场景。
本篇文章总结归纳一下自己在canvas开发的认知和可能遇到的常见问题。
至于基础知识和常见API使用,大家自行学习,这里不予复述。

重要概念

HTML5 <canvas> 元素用于图形的绘制,生成图形容器,必须通过JS脚本来完成。
canvas绘图时相对于一根画笔,每一步骤的绘制是基于当前状态的(位置、颜色等)
canvas api文档
canvas.width/height :用来控制Canvas画布绘制区域的宽高。需根据实际屏幕环境设置,否则绘制的图形会失真。
canvas.getContext():返回canvas的绘制上下文, 实际绘制依赖于该对象。
canvas.toBlob():可以对Canvas图像生成对应的Blob对象。
canvas.toDataURL():返回base64 data图片数据。

<canvas> 标签

HTML5 中的<canvas> 标签只会生成图形容器,图形绘制必须通过JS脚本来完成。

Canvas宽高与CSS宽高

<canvas width="600" height="300" style="width: 300px; height: 150px"></canvas>
  • style中的width/height代表canvas元素在界面上所占据的宽/高, 即样式上的CSS宽高。
  • 属性中的width/height **则代表canvas实际像素的宽高。**用来控制Canvas画布绘制区域的宽高。不设置宽高时,会有默认的宽高(300*150),一般建议设置好图形宽高限度绘制区域。


当使用Canvas API绘制图形时使用的坐标、尺寸大小是基于Canvas宽高属性的,而与CSS样式宽高无关。
而CSS宽高则决定canvas图形的视觉显示大小,canvas画布的宽高会等比例缩放成CSS宽高显示。

实际使用时,尽量避免这种因尺寸不一致比例缩放渲染,导致的图形模糊、锯齿化等问题。

绘制上下文

canvas.getContext():返回canvas的绘制上下文, 实际绘制依赖于该对象。
本文主要讨论二维平面绘制:const context =``canvas.getContext('2d');

实际画布绘图时每一路径的绘制过程是基于绘制上下文的当前状态的。
绘制过程相当于只使用一根画笔作画,每一次路径的绘制点、颜色等样式是基于绘制上下文的当前状态(位置、颜色、粗细等)。
对于非连续的路径绘制,绘制过程要用context.beginPath()声明,这样可以使绘制样式不会被覆盖影响。

比如:

context.strokeStyle = 'blue';
context.moveTo(10, 10);
context.lineTo(100, 10);
context.stroke();

context.strokeStyle = 'red';
context.moveTo(100, 10);
context.lineTo(10, 50);
context.stroke();

样式被覆盖,绘制效果如下:
image.png
而用context.beginPath()声明路径,代码:

context.beginPath();
context.strokeStyle = 'blue';
context.moveTo(10, 10);
context.lineTo(100, 10);
context.stroke();

context.beginPath();
context.strokeStyle = 'red';
context.moveTo(100, 10);
context.lineTo(10, 50);
context.stroke();

则两条线的样式不受影响,绘制效果如下:
image.png

context.canvas

这个在很多时候需要在绘制时获取当前画布信息时非常有用。CanvasRenderingContext2D.canvas是一个只读属性,可以返回当前上下文源自哪个<canvas>元素,并获取画布的width/height等属性信息。

context.clearRect()

在实际开发时经常会使用到的api,清除制定的绘制区域,否则在同一区域重复绘制会发生图形叠加。

Canvas与SVG的区别 

Canvas和SVG是当前HTML5中主要使用的图形绘制技术,前者提供画布标签和绘制API,后者是一整套独立的矢量图形语言,使用 XML 格式定义图像。

  1. Canvas通过JS绘制图形,只有当个HTML元素;而SVG使用 XML 格式定义图形,生成的图形包含多种图形元素(Path、Line、Rect)。
  2. Canvas绘制基于像素级控制;SVG则基于内部图形元素操作控制;
  3. Canvas是像素级渲染,依赖分辨率;SVG则是矢量图形,缩放时图形质量不会失真;
  4. 事件交互:Canvas中,事件只能注册到<canvas>标签上,但通过事件委托,可以细化到像素点(x,y)的交互;SVG则可以为某个元素附加 单独的JavaScript 事件处理器,但也只能控制细化在图形元素上。
  5. Canvas适合小面积、大数据应用场景;SVG适合大面积、小数量应用场景(图像元素少)。

Canvas适用场景:适合像素处理,动态渲染和大数据量绘制; 适合图像密集型的游戏;
SVG适用场景:适合静态图片展示,高保真文档查看和打印的应用场景。

常见问题

移动端绘制canvas模糊问题

现象

如下图:
image.png
上图中,在未做兼容移动端处理时,绘制的canvas刻度组件在iphone 6s机型上,canvas图形和文字模糊失真。兼容处理后图形质量得以保证。
以上刻度组件参考我的另一篇文章,地址:「采用Canvas绘制一个可配置的刻度(尺)组件

原因

关于移动端高清屏DPR、图片模糊、移动端适配等问题,不清楚的童鞋可以参考「关于移动端适配,你必须要知道的」这篇文章,讲的比较详细。这里不再赘述,本文章只处理移动端Canvas模糊问题。
在移动端高清屏幕上,经常会遇到Canvas图形模糊的问题。本质上跟移动端图片模糊问题是一样的。canvas绘制成的图像跟也是位图,在dpr > 1的屏幕上,位图的一个像素可能由多个物理像素来渲染,然而这些物理像素点并不能被准确的分配上对应位图像素的颜色,只能取近似值,所以在dpr > 1的屏幕上就会模糊。
在PC端绘制canvas图形,我们都直接把1个canvas像素直接等于1px的css像素处理,这没有问题,因为目前PC端屏幕dpr都是1。而在dpr > 1的移动端屏幕上就不能直接这样处理。

解决

解决方案当然还是从dpr入手。

  1. 通过window.devicePixelRatio获取当前设备屏幕的dpr;
  2. 首先获取或设置Canvas容器的宽高;
  3. 根据dpr,设置canvas元素的宽高属性;在dpr = 2时相当于扩大画布2倍;
  4. 通过context.scale(dpr, dpr)缩放Canvas画布的坐标系。在dpr = 2时相当于把canvas坐标系也扩大了两倍,这样绘制比例放大了2倍,之后canvas的实际绘制像素就可以按原先的像素值处理。

在渲染到屏幕时,扩大的画布图形又等比例缩放渲染到canvas容器中。从而保证canvas图形的质量。

// 获取dpr
const dpr = window.devicePixelRatio; 
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 获取Canvas容器的宽高
const { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect();
// 根据dpr,设置Canvas的宽高,使1个canvas像素和1个物理像素相等
canvas.width = dpr * cssWidth;
canvas.height = dpr * cssHeight;
// 根据dpr,设置canvas元素的宽高属性
ctx.scale(dpr,dpr);

canvas drawImage()参数问题,移动端图片模糊问题

canvas的drawImage() 函数有个特别容易混淆搞错的地方。它的5参数和9参数用法的参数位置是不同的。实际开发中没注意到这一点,会让自己特别困惑问题出在哪!汗!

drawImage()方法有一个非常怪异的地方,大家一定要注意,那就是5参数和9参数用法的参数位置是不一样的,这个和一般的API有所不同。一般API可选参数是放在后面。但是,这里的drawImage()使用9个参数时候,可选参数sx,sy,sWidth和sHeight是在前面的。如果不注意这一点,有些表现会让你无法理解。

且drawImage()函数插入的图形在移动端dpr >1屏幕同样会有图片模糊的问题。
在移动端通过drawImage()载入另一个已绘制的Canvas元素时,也要注意对另一个canvas元素做兼容处理,还需要注意两者坐标系的不同。

// 设置canvas_bg宽高
canvas_bg.width = (config.unit * (scale_len - 1) + config.width) * dpr;
canvas_bg.height = config.height * dpr;
ctx_bg.scale(dpr, dpr);

...

// 初始化开始位置
point_x = (config.def - config.start) / config.capacity * config.unit;
//在主画布ctx上,通过drawImage()插入另一个canvas_bg画布;
ctx.drawImage(canvas_bg, point_x * dpr, 0, config.width * dpr, config.height * dpr, 0, 0, config.width, config.height);

上面的代码中, canvas_bg画布同样需要处理上面提到的canvas模糊问题;在主画布ctx上,通过drawImage()插入另一个canvas_bg画布图形时,需要注意此时两者坐标系比例的不同,此时canvas_bg的坐标系是根据dpr缩放后的。

当canvas绘制尺寸或drawImage插入图像、getImageDate获取图形资源等尺寸大于某个阈值时,可能会出现绘制空白问题。

在实际开发中遇到,canvas绘制尺寸或drawImage插入图像、getImageDate获取图形资源等尺寸大于某个阈值时,渲染出来的图片整个都是空白。这个具体的阈值不确定,跟运行环境有关。但这应该也是drawImage绘制的一个不知何时爆发的隐患。
比如下图,绘制的刻度尺画布尺寸过大,截取后渲染到主画布上,整个刻度空白,但不影响交互。
image.png

canvas getImageData 跨域问题

只要能够在网页中正常显示出来的跨域图片,就可以使用canvas的drawImage() API绘制出来。但是如果想通过getImageData()方法获取图片的完整的像素信息,转换成本地输出时,则多半会出现跨域问题。

解决:

1. 是页面与服务端开启允许跨域;

2. 给图片设置允许跨域,`img.setAttribute('crossOrigin', 'anonymous');`