Skip to main content

Canvas

Canvas 上绘制图形是一种即时模式(immediate mode),一旦在 Canvas 上绘制了图形之后,Canvas 将不再知道这个图形的任何信息。被绘制的图形是可见的,但是你不能够操作这个图形,留在上面的只是一些像素

这是 Canvas 和 SVG 不同的地方,SVG 图形是可以被单独操纵的,也可以被重新绘制。在 HTML5 canvas 中如果你想修改绘制的图形,你需要重新绘制所有的东西

用 canvas 绘制图形的基本步骤:

  1. 等待页面 DOM 元素加载完毕
  2. 获取 canvas 元素的引用
  3. 从 canvas 元素中获取 2D 上下文(context)
  4. 从 2D 上下文中使用绘制函数绘制图形
// ...
window.onload = function () {
drawExamples();
};
function drawExamples() {
const canvas = document.getElementById("myCanvas");
const context = canvas.getContext("2d");
context.fillStyle = "#000";
context.fillRect(100, 100, 100, 100);
}

Canvas 使用的是 W3C 坐标系,坐标空间参考如下:

canvas坐标空间

基础图形分类

详细内容参考 使用 canvas 来绘制图形 - MDN

矩形

context.fillStyle = "#ff0000";
context.fillRect(10, 10, 100, 100);
// context.strokeStyle = "#0000ff";
// context.strokeRect(30, 20, 120, 110);

三角形

ctx.beginPath();
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
// 会自动让所有形状闭合,区别与stroke
ctx.fill();

线条

ctx.beginPath();
// 定义直线的起点坐标
ctx.moveTo(10, 10);
// 定义直线的终点坐标
ctx.lineTo(50, 10);
// 沿着坐标点顺序的路径绘制直线
// 设置虚线
// cxt.setLineDash([10, 20]) // 10px间隔20px
ctx.stroke();
// 关闭当前的绘制路径
ctx.closePath();

圆弧/椭圆/圆形

ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
// ctx.arc(x, y, r, sAngle, eAngle, counterclockwise);
// x,y:圆心坐标; r:半径大小;
// sAngle:起始角,以弧度计(弧的圆形的三点钟位置是 0 度)
// eAngel:结束角,以弧度计
// counterclockwise:可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针

文字

const text = "Hello world";
ctx.font = "48px serif";
// 文本对齐方式
ctx.textAlign = "center";
// 填充颜色
ctx.fillStyle = "#000";
// 设置内容和坐标
ctx.fillText(text, 10, 50);
// 空心字
// ctx.strokeText(text, 10, 50);
// 获取文本宽度
// ctx.measureText(text).width

图像处理

可以借助 drawImage 加载图片,存在跨域的问题,解决方法参考下文的一些问题

// 正常加载图片
ctx.drawImage(image, x, y, width, height);
// 图片裁减,s前缀参数标识了裁减的范围
ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);

基础 api

参考 Canvas API

常见方法如下:

  • fill 填充当前绘图(对应着面)
  • stroke 绘制已定义的路径(对应着线)
  • beginPath 起始一条路径或者是重置当前路径(对应着线)
  • moveTo(x,y) 把路径移动到动画中的指定点,不创建线条。x、y 对应着路径的目标位置的 x、y 坐标
  • lineTo 添加一个新点,然后在画布中创建从上一个点(lineTo 或 moveTo 中的坐标点)到该点的线条
  • closePath 创建当前点到开始点的路径。一般用于闭合多边形
  • clip 从原始画布剪切任意形状和尺寸的区域
  • rect 创建矩形
  • fillrect 绘制被填充的矩形
  • strokeRect 绘制矩形(无填充)
  • clearRect 在指定的矩形内清除指定的像素
  • arc 创建弧/曲线(主要是创建圆形或部分圆)
  • arcTo 创建两切线之间的弧/曲线
  • quadraticCurveTo(cpx,cpy,x,y) 创建二次贝塞尔曲线。(cpx,cpy)控制点的坐标;(x,y)结束点的坐标。
  • bezierCurveTo 创建三次贝塞尔曲线

图形转换

做图形转换需要先熟悉状态的保存和恢复 saverestore,在做变换之前使用 save,变换之后只使用 restore,目的是重置坐标系原点、方向和缩放比例。参考 变形 Transformations

  • ctx.translate(x,y) 平移
  • ctx.scale(scalewidth,scaleheight) 缩放
  • ctx.rotate(deg) 旋转
  • ctx.transform 矩阵变换

动画

可以借助定时器或 requestAnimationFrame,本质都是擦除画布再重新绘制

图层

没有图层概念,但可以模拟,参考 https://vimcaw.github.io/blog/2018/02/10/%E7%94%A8Canvas%E5%AE%9E%E7%8E%B0%E5%9B%BE%E5%B1%82/

比如 fabricjs 中分为了交互图层和渲染图层

常见效果的实现

贝塞尔曲线

// ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
ctx.beginPath();
ctx.moveTo(50, 20);
ctx.bezierCurveTo(230, 30, 150, 60, 50, 100);
ctx.stroke();

ctx.fillStyle = "blue";
// start point
ctx.fillRect(50, 20, 10, 10);
// end point
ctx.fillRect(50, 100, 10, 10);

ctx.fillStyle = "red";
// control point one
ctx.fillRect(230, 30, 10, 10);
// control point two
ctx.fillRect(150, 70, 10, 10);

渐变色

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, "green");
gradient.addColorStop(1, "white");
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);

阴影和填充

// 水平位移
ctx.shadowOffsetX = 10;
// 垂直位移
ctx.shadowOffsetY = 10;
// 设置模糊度
ctx.shadowBlur = 5;
// 设置阴影颜色
ctx.shadowColor = "rgba(0,0,0,0.5)";
// ctx.fillstyle = "#000"
// ctx.fillRect(100, 100, 100, 100)

阴影绘制是比较耗费性能的

进阶

  • 导出内容 toDataUrl
  • 组合图形
  • Path2D。允许你在 canvas 中根据需要创建可以保留并重用的路径
  • 像素处理
  • 合成模式

性能优化

  • 脏矩形更新
  • 分层渲染
  • 双缓冲画布
  • web worker 离屏渲染

封装库

  • Fabric.js
  • Konva

一些问题

canvas vs svg

  • canvas:基于像素,适合绘制图形较多、交互更复杂的场景,而且更利于实现一些视觉特效
  • svg:为矢量图,缩放不模糊,内存占用更低,标签利于爬虫

图片、文字模糊怎么解决?比如在 retina 屏幕上 canvas 的内容显示变模糊

原因:比如屏幕的宽是 720 像素的, 而这个 canvas 是按照小于 720 像素画出来的, 所以在 720 像素的屏幕上显示时, 这个 canvas 的内容其实是经过拉伸的, 所以会出现模糊和锯齿

function getPixelRatio(context) {
// 获取 canvas 的 backingStorePixelRatio 值
const backingStore =
context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio ||
1;
// 若 devicePixelRatio 不存在,默认为 1
return (window.devicePixelRatio || 1) / backingStore;
}

function adjustCanvas(canvas, context) {
const ratio = getPixelRatio(context);
const oldWidth = canvas.width;
const oldHeight = canvas.height;
// 按照比例放大 canvas
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
// 将 canvas 调整成原来大小
canvas.style.width = oldWidth + "px";
canvas.style.height = oldHeight + "px";
}

在 ios6 下,webkitBackingStorePixelRatio 的值为 2,此时 canvas 模糊的问题不存在或是得到了很大的缓解,这是因为此 iphone 的 devicePixelRatio 也正好为 2 时,canvas 的真实像素量正好等于屏幕的物理像素量,此时正好匹配,也就不存在模糊的问题了。所以我们进行处理时,要重新计算我们需要的像素量的比例,不能单纯使用 devicePixelRatio,而是应该使用 devicePixelRatio / webkitBackingStorePixelRatio 的比值。参考自 解决 HTML5 Canvas 在高分屏下的模糊问题

canvas 中画线条,线条效果比预期宽 1 像素

canvas 画线原理:以指定坐标为中心向两侧画线,也就是两侧各画宽的一半,但如果坐标在(1.0),画 1px 相当于左右画 0.5,停留在 (1.5, 0) 的话也会自动将延展到 2。所以当绘制奇数像素宽度的线条时,就会出现 1 像素问题,其实就是半像素定位(half-pixel positioning)导致的。参考 Canvas 绘制 1 px 直线模糊(非高清屏)的问题

// 竖线
function drawLine(ctx, x, y1, y2, width) {
// 当线宽为偶数时,坐标应指定为整数。否则坐标应指定为整数+0.5px
let newx = width % 2 === 0 ? Math.floor(x) : Math.floor(x) + 0.5;
ctx.lineWidth = width;
ctx.moveTo(newx, y1);
ctx.lineTo(newx, y2);
}

优化文字锯齿

答:使用更高的分辨率,参考模糊的解决方案

什么是 canvas 污染(tainted)?

答:跨域限制。当使用 drawImage 方法绘制一个不同源的图像时,此时并不会报错,但是 canvas 会变成 tainted (被污染),之后如果在当前被污染的 canvas 上调用这些方法(toDataURL、toBlob、getImageData)时就会抛出 SecurityError 的错误

怎么解决?配置服务端响应头支持跨域请求的域名、请求方法等(CORS),然后修改图像的 crossOrigin(img.crossOrigin="anonymous"

canvas 图片跨域怎么解决?

答:一个是上面的方法,另外还有通过代理绕开跨域限制,以及转成 base64 再加载

实践

  • 实现圆角矩形
function arcRect(ctx, x, y, w, h, r) {
// 右上角弧线
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
// 右下角弧线
ctx.moveTo(x + w, y + r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
// 左下角弧线
ctx.moveTo(x + w - r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
// 左上角弧线
ctx.moveTo(x, y + h - r);
ctx.arcTo(x, y, x + r, y, r);
}

参考