Canvas
Canvas 上绘制图形是一种即时模式(immediate mode),一旦在 Canvas 上绘制了图形之后,Canvas 将不再知道这个图形的任何信息。被绘制的图形是可见的,但是你不能够操作这个图形,留在上面的只是一些像素
这是 Canvas 和 SVG 不同的地方,SVG 图形是可以被单独操纵的,也可以被重新绘制。在 HTML5 canvas 中如果你想修改绘制的图形,你需要重新绘制所有的东西
用 canvas 绘制图形的基本步骤:
- 等待页面 DOM 元素加载完毕
- 获取 canvas 元素的引用
- 从 canvas 元素中获取 2D 上下文(context)
- 从 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 来绘制图形 - 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
创建三次贝塞尔曲线
图形转换
做图形转换需要先熟悉状态的保存和恢复 save
和restore
,在做变换之前使用 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);
}