canvas画布栅格、样式拉伸、1px等问题

2,284 阅读5分钟

这些是初步了解canvas时,都会遇到的问题。

案例:在canvas上画一个长方形

html

<canvas id="myCanvas"> 您的浏览器不支持canvas</canvas>

css:

#myCanvas{
            width: 400px;  height: 400px;
            border: 1px solid red;
        }

js:

let c = document.getElementById("myCanvas")
console.log(c.width ,c.height)
 
let ctx = c.getContext('2d');
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);

看似很简单的代码,打开页面之后就出问题了:

1,我不是画的宽度150 高100的矩形么,怎么高度比宽度还长?

2,坐标不是x:10 y:10 么,怎么看起来y比x的间距大呢?而且上下边看起来比较模糊

1.png

变形问题原因:样式缩放导致

解释原因前先看官方解释:

栅格

画布栅格(canvas grid)以及坐标空间。

如图所示,canvas元素默认被网格所覆盖。通常来说网格中的一个单元相当于canvas元素中的一像素。

栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。所以图中蓝色方形左上角的坐标为距离左边(X轴)x像素,距离上边(Y轴)y像素(坐标为(x,y))。

Canvas_default_grid.png

注: 虽然官网说一个格子相当于1px,但我觉得不排除某些设备上,一个格子里有多个像素点。类似移动端设备1px问题一样。

canvas除了默认属性外,只有两个attr,分别是width和height,表示画布内部的像素。

比如:

<canvas id="myCanvas" width="400px" height="500px"> 您的浏览器不支持canvas</canvas>

表示这个canvas里横向有400像素,纵向有500像素,也就是栅格里所谓的格子。

canvas默认300 * 150像素

css样式里设置的宽高,只是表示canvas如何显示,跟普通的dom一样,不能决定画布里的像素点。所以当样式是400 * 400,而canvas默认是300 * 150 时,就会导致拉伸或者说缩放scale,(可能用缩放scale更准确)

<style>
#myCanvas{
            width: 400px;  height: 400px;
            border: 1px solid red;
        }
</style>
<canvas id="myCanvas"> 您的浏览器不支持canvas</canvas>

此时就相当于,把300 * 150 展示出来的东西,按样式的比例缩放了,宽度放大约1.33倍(400/300),高度放大约2.67倍(400/150)。也可以理解为像素数量不变,面积大了之后像素密度变小了。

所以我们设置矩形的宽150高100 本身没问题,只不过我们看到的是用样式缩放后的结果。如果我们把canvas加上 width 和 height 就可以了:

<canvas id="myCanvas"  width="400px" height="400px"> 您的浏览器不支持canvas</canvas>

效果:

2.png

1px问题:

移动端各种不同分辨率的屏幕,对1px显示不一样。canvas 和这个问题类似。

官方举例:

canvas-grid.png

        如果你想要绘制一条从 (3,1) 到 (3,5),宽度是 1.0 的线条,你会得到像第二幅图一样的结果。实际填充区域(深蓝色部分)仅仅延伸至路径两旁各一半像素。而这半个像素又会以近似的方式进行渲染,这意味着那些像素只是部分着色,结果就是以实际笔触颜色一半色调的颜色来填充整个区域(浅蓝和深蓝的部分)。这就是为何宽度为 1.0 的线并不准确的原因。

        要解决这个问题,你必须对路径施以更加精确的控制。已知粗 1.0 的线条会在路径两边各延伸半像素,那么像第三幅图那样绘制从 (3.5,1) 到 (3.5,5) 的线条,其边缘正好落在像素边界,填充出来就是准确的宽为 1.0 的线条。

分析:

  1. 如果一个像素格子只渲染一部分,那么格子里其余的部分会做近似渲染,所以我们看到的线条会感觉到模糊。
  2. 可以通过小数来确定具体的坐标,保证1px的格子宽度。

不同方式绘制1px:

测试环境: chrome 95+

let ctx = c.getContext('2d');
// 绘制宽度1px的矩形   
ctx.fillStyle = 'green';
ctx.fillRect(30, 10, 1, 40);

// 绘制宽度1px的线条
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(40, 10);
ctx.lineTo(40, 50);
ctx.stroke();

ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(50.5, 10);
ctx.lineTo(50.5, 50);
ctx.stroke();

分别用fillRect画1px的矩形 和 设置lineWidth为1px 画线段。结果:

3.png

明显看到 第二条线段虽然 lineWidth为1px,但是比宽度为1px的矩形要宽,而且是灰颜色。这个就是上面说到的1px问题,本来渲染时 1px只占用1个格子,结果占用了2个,所以看起来要宽。而且可能是因为近似渲染问题,黑色的线段变成了灰色。

第三条线段把坐标移动到了格子中间,保证在一个格子里去渲染,而且颜色是默认的黑色。

接下来看看其他的情况

画1px线段时,坐标是小数的情况:

测试环境: chrome 95+

ctx.lineWidth = 1;
for (let i = 0; i < 10; i++) {
    ctx.beginPath();
    ctx.moveTo(50+i*10+i/10, 10);
    ctx.lineTo(50+i*10+i/10, 50);
    ctx.stroke();
}

画10条线,坐标位置从0 - 1 递增,结果:

4.png 可以看到,当坐标的小数是 0.5 时,宽度才是1px并且是黑色,

其他的根据坐标位置,由近及远颜色渐变,并且是2px

坐标是正整数,lineWidth宽度从1-10递增:

测试环境: chrome 95+

for (var i = 0; i < 10; i++) {
    ctx.lineWidth = 1 + i;
    ctx.beginPath();
    ctx.moveTo(20 + i * 14, 10);
    ctx.lineTo(20 + i * 14, 50);
    ctx.stroke();
}

5.png 可以看到宽度是奇数的线条都不清晰,这是因为奇数多占用一个像素格子,但是只渲染了格子的一部分。

偶数的线条刚刚好能把格子渲染完成。

简单的解决办法:

for (var i = 0; i < 10; i++) {
    ctx.lineWidth = 1 + i;
    ctx.beginPath();
    let x = 20 + i * 14
    if(ctx.lineWidth % 2 != 0){
        ctx.moveTo(x + 0.5, 10);
        ctx.lineTo(x + 0.5, 50);
    }else{
        ctx.moveTo(x, 10);
        ctx.lineTo(x, 50);
    }
    
    ctx.stroke();
}

结果明显是我们想要的样子:

6.png

canvas笔记一,完毕。