Intro to Canvas
画布尺寸
Canvas 有两种 width
和 height
:
- 一种是 width、height 属性,一般称其为 画布尺寸,即图形绘制的地方。默认值分别为
300px
、150px。
例如:
<canvas id="canvas" width="300" height="150"></canvas>
- 另一种是 CSS 样式里的 width、height 属性,可通过内联样式、内部样式表或外部样式表设置。一般称其为 画板尺寸,用于渲染绘制完成的图形。默认值为空。
例如:
<canvas id="canvas" style="width: 300px; height: 150px;"></canvas>
或
<style>
#canvas {
width: 300px;
height: 150px;
}
</style>
画布尺寸 | 画板尺寸 | 说明 |
---|---|---|
已设置 | 未设置 | 画板尺寸随画布尺寸改变 |
未设置 | 已设置 | 画板尺寸将不再随画布尺寸而改变 |
已设置 | 已设置 | 画板尺寸将不再随画布尺寸而改变 |
如果两者设置的尺寸不一样时,就会产生一个问题,渲染时画布要通过缩放来使其与画板尺寸一样,那么画布上已经绘制好的图形也会随之缩放,随之导致变形失真。
下面为绘制从原点到 200*200
的直线:
class Demo extends React.Component { componentDidMount() { const canvasList = document.querySelectorAll('.canvas'); canvasList.forEach(item => { const context = item.getContext('2d'); this.renderPath(context); }); } renderPath(ctx) { ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(200, 200); ctx.stroke(); } render() { return ( <> <p>1、已设置画布宽高属性(200px * 200px),未设置画板样式宽高,画板尺寸随画布尺寸改变</p> <canvas id="canvas1" className="canvas" width="200" height="200"></canvas> <p>2、未设置画布宽高属性,但已设置画板样式宽高(200px * 200px)</p> <canvas id="canvas2" className="canvas" style={{ width: 200, height: 200 }}></canvas> <p>3、设置画布宽高属性(300 * 300),设置画板样式宽高(200 * 200)</p> <canvas id="canvas3" className="canvas" width="300" height="300" style={{ width: 200, height: 200, border: '1px solid #000' }}></canvas> <p>4、设置画布宽高属性(200 * 200),设置画板样式宽高(300 * 300)</p> <canvas id="canvas4" className="canvas" width="200" height="200" style={{ width: 300, height: 300, border: '1px solid #000' }}></canvas> </> ); } }
高清分辨率
上面说过,避免图形变形失真,要保持画布尺寸和画板尺寸一致。
这只是针对分辨率不高的设备而言,其 window.devicePixelRatio
为 1。而高分辨率屏幕,它的 window.devicePixelRatio
大于 1。
Canvas 绘制的图形是位图,即 栅格图像 或 点阵图像,当将它渲染到高清屏时,会被放大,每个像素点会用 window.devicePixelRatio
平方个物理像素点来渲染,因此图片会变得模糊。
解决方法:
- 通过
window.devicePixelRatio
获取当前设备屏幕的 DPR - 获取或设置 Canvas 容器的画板尺寸
- 根据 DPR,设置 Canvas 元素的宽高属性(在 DPR 为 2 时,相当于扩大画布的两倍)
- 通过
context.scale(dpr, dpr)
缩放 Canvas 画布的坐标系,在 DPR 为 2 时相当于把 Canvas 坐标系也扩大了两倍,这样绘制比例放大了两倍,之后 Canvas 的实际绘制像素就可以按原先的像素值处理
function Demo() { const hdCanvasRef = useRef(null); const ldCanvasRef = useRef(null); const renderPath = context => { context.beginPath(); context.moveTo(0, 0); context.lineTo(200, 200); context.lineWidth = context.lineWidth; context.stroke(); }; // 绘制高清晰度画布 const initHDCanvas = () => { const canvas = hdCanvasRef.current; // 获取 DPR const dpr = window.devicePixelRatio; const ctx = canvas.getContext('2d'); // 获取容器高度 const { width, height } = hdCanvasRef.current.getBoundingClientRect(); // 根据 DPR 设置 Canvas 的宽高,使 1 个 Canvas 元素和 1 个物理像素相等 canvas.width = dpr * width; canvas.height = dpr * height; // 根据 DPR 设置 Canvas 的宽高属性 ctx.scale(dpr, dpr); renderPath(ctx); }; // 绘制低清晰度画布 const initLDCanvas = () => { const ctx = ldCanvasRef.current.getContext('2d'); renderPath(ctx); }; useEffect(() => { initHDCanvas(); initLDCanvas(); }, []); return ( <> <p>已适配 画布像素:画板像素(物理像素)= 1:1</p> {/* 高清画布 */} <canvas ref={hdCanvasRef} style={{ border: '1px solid #000', width: 200, height: 200 }}></canvas> <br /> <p>未适配 画布像素:画板像素(物理像素)= {window.devicePixelRatio}:1</p> {/* 普通画布 */} <canvas ref={ldCanvasRef} width="200" height="200" style={{ border: '1px solid #000' }}></canvas> </> ); }
样式设置的 width
是的元素内容宽度,不包括内边距、边框、外边距的,而 clientWidth
包括内边距,不包括边框、外边距、滚动条的(如果有)。
画布状态
CanvasRenderingContext2D 渲染环境包含了多种绘图的样式状态(属性有线的样式、填充样式、阴影样式、文本样式)。
Canvas 的 API 提供了两个名叫 CanvasRenderingContext2D.save()
和 CanvasRenderingContext2D.restore()
的方法,用于保存及恢复当前 Canvas 绘画环境的所有属性。其中 CanvasRenderingContext2D.save()
可以保存当前状态,而 CanvasRenderingContext2D.restore()
可以还原之前保存的状态。
这两个方法在绘图中有着重要的作用,比如我们在绘图的时候需要使用多种颜色,颜色需要不时的切换。那么使用 save()
和 restore()
方法即可比较方便地实现此功能。
save
CanvasRenderingContxt2D.save()
是 Canvas 2D API 通过将当前状态放入栈中,保存 Canvas 全部状态的方法。
function Demo() { const canvasRef = useRef(null); useEffect(() => { const ctx = canvasRef.current.getContext('2d'); // 保存默认的状态 ctx.save(); ctx.fillStyle = 'green'; ctx.fillRect(10, 10, 100, 100); // 还原到上次保存(save)的默认状态 ctx.restore(); ctx.fillRect(150, 75, 100, 100); }, []); return <canvas ref={canvasRef} width="200" height="200"></canvas>; }
restore
CanvasRenderingContxt2D.restore()
是 Canvas 2D API 通过在绘图状态栈中弹出顶端的状态,将 Canvas 恢复到最近的保存状态的方法。 如果没有保存状态,此方法不做任何改变。
当该方法和 save
一起使用时,恢复到 ctx.save
保存时的状态。
状态和非状态
在 Canvas 环境中绘图时,可以利用所谓的绘图堆栈状态。每个状态随时存储 Canvas 上下文数据。
下面是存储在状态堆栈的数据列表。
- 当前的坐标变换(变换矩阵)信息,比如旋转或平移时使用的
rotate()
和setTransform()
方法 - 当前剪贴区域
- 图形上下文对象(
CanvasRenderingContext2D
)的当前属性值
CanvasRenderingConext2D
的当前属性值主要包括:
属性 | 默认值 | 描述 |
---|---|---|
canvas | - | 取得画布 <canvas> 元素 |
fillStyle | #000000 | 填充路径的当前的颜色、模式或渐变 |
strokeStyle | #000000 | 指定线段颜色 |
globalCompositeOperation | source-over | 指定颜色如何与画布上已有颜色组合(合成) |
lineCap | butt | 指定线段端点的绘制方式 |
lineJoin | miter | 指定线段端点的绘制方式 |
lineWidth | 1 | 绘制线段的宽度 |
miterLimit | 10 | 当 lineJoin 为 miter 时,这个属性指定斜连接长度和二分之一线宽的最大比率 |
shadowColor | rgba(0, 0, 0, 0) | 指定阴影颜色 |
shadowBlur | 0 | 指定阴影模糊度 |
shadowOffsetX | 0 | 指定阴影水平偏移值 |
shadowOffsetY | 0 | 指定阴影垂直偏移值 |
save()
和restore()
方法允许你保存和恢复一个CanvasRenderingContext2D
对象的状态。save()
把当前状态推入到绘图堆栈中,而restore()
从绘图堆栈中的顶端弹出最近保存的状态,并且根据这些存储的值来设置当前绘图状态。
简单来说,save()
主要用来保存目前 Canvas 的状态,通过 save()
函数它会将目前 Canvas 的状态推到绘图堆栈中;而 restore()
函数就是从绘图堆栈中弹出上一个 Canvas 的状态。
应用实例
制作一个扇形
在实际使用当中,save()
和 restore()
的使用还是非常广泛的,特别是涉及到坐标系统的变换和图形变换方面。
function Demo() { const canvasRef = useRef(null); /** * * @param ctx Canvs 绘图上下文 * @param x 位移目标点横坐标 * @param y 位移目标点纵坐标 * @param r 圆弧半径 * @param sDeg 旋转起始角度 * @param eDeg 旋转终点角度 */ const drawSector = (ctx, x, y, r, sDeg, eDeg) => { // 初始保存 ctx.save(); // 位移到目标点 ctx.translate(x, y); ctx.beginPath(); // 画出圆弧 ctx.arc(0, 0, r, sDeg, eDeg); // 再次保存以备旋转 ctx.save(); // 旋转至起始角度 ctx.rotate(eDeg); // 移动到终点,准备连接终点与圆心 ctx.moveTo(r, 0); // 连接到圆心 ctx.lineTo(0, 0); // 还原 ctx.restore(); //旋转至起点角度 ctx.rotate(sDeg); // 从圆心连接到起点 ctx.lineTo(r, 0); ctx.closePath(); // 还原到最初保存的状态 ctx.restore(); }; const drawScreen = () => { const ctx = canvasRef.current.getContext('2d'); const deg = Math.PI / 180; const payload = { x: 300, y: 150, r: 80, sDeg: [30, 111, 190, 233, 280, 345], eDeg: [111, 190, 233, 280, 345, 30], style: ['#f00', '#0f0', '#00f', '#789', '#abcdef'], }; for (let i = 0; i < payload.sDeg.length; i++) { drawSector(ctx, payload.x, payload.y, payload.r, payload.sDeg[i] * deg, payload.eDeg[i] * deg); ctx.fill(); ctx.fillStyle = payload.style[i]; } }; useEffect(() => { drawScreen(); }, []); return <canvas ref={canvasRef}></canvas>; }