为什么 1px 线条要 +0.5 才清晰?这个问题问到了 Canvas 渲染最本质的像素原理,我用最简单、最直观的方式给你讲明白:
为什么 1px 线条要 +0.5 才清晰?
一句话结论: 因为 Canvas 的坐标点,落在像素格子的「缝隙」上,而不是格子中心。
1. 先看屏幕像素是怎么排列的
你可以把屏幕想象成棋盘格:
(0,0) (1,0) (2,0)
┌─────────┬─────────┬───
│ │ │
│ 像素0,0 │ 像素1,0 │
│ │ │
(0,1)├────────┼─────────┼───
│ │ │
│ 像素0,1 │ 像素1,1 │
重点:
- 坐标 (x,y) 是像素格子的角点
- 不是像素中心!
2. 画 1px 线条时发生了什么?
Canvas 画线规则: 线条以坐标为中心,向左右各延伸 0.5px
比如你画:
ctx.lineWidth = 1
ctx.strokeRect(10, 10, 50, 50)
边框线会从:
- 左:10.0 向左 0.5px → 9.5
- 右:10.0 向右 0.5px → 10.5
结果: 线条跨在两个像素中间!
- 一半在 9 号像素
- 一半在 10 号像素
浏览器只能做一件事: 👉 两个像素都半透明模糊显示 于是你看到的就是:发虚、模糊、灰线
3. 为什么 +0.5 就清晰了?
当你写:
ctx.strokeRect(10.5, 10.5, 50, 50)
线条中心在 10.5
- 向左 0.5px → 10.0
- 向右 0.5px → 11.0
刚好完美占满第 10 号像素格子!
没有跨像素,没有半透明 → 显示为清晰锐利的 1px 实线
4. 超直观总结
- 坐标是整数 → 线落在像素缝隙 → 模糊
- 坐标是 x.5 → 线落在像素正中心 → 清晰
| 写法 | 线条位置 | 效果 |
|---|---|---|
| strokeRect(10,10,w,h) | 跨两个像素 | 模糊、发灰 |
| strokeRect(10.5,10.5,w,h) | 刚好填满一个像素 | 清晰、锐利 |
5. 只有 1px 线需要这样吗?
是的!
- lineWidth = 1 → 必须 +0.5
- lineWidth ≥ 2 → 不需要 因为偶数宽度可以刚好对齐整数坐标,不会跨像素。
在Canvas中:
- 如果不偏移0.5,线会跨在两个像素之间,导致抗锯齿模糊
- 如果偏移0.5(从整数坐标移到 x.5),线会正好在一个像素中心,显示清晰
分成两种情况:
- 水平线(左右延伸):y 坐标加 0.5,x 保持整数
- 垂直线(上下延伸):x 坐标加 0.5,y 保持整数
- 因为:
- 水平线跨越多个 x 像素,但只占 1 个 y 像素高度。为了让这 1px 高度正好落在单个像素行内,y 需要是 x.5。
- 垂直线跨越多个 y 像素,但只占 1 个 x 像素宽度。为了让这 1px 宽度正好落在单个像素列内,x 需要是 x.5。
- 因为:
斜线模糊是几何决定的:它斜着穿过像素网格,无论如何偏移(哪怕是 x+0.5, y+0.5),都必然跨过多个像素的边界,抗锯齿不可避免。
- 横/竖线:只需在垂直于线的方向对齐像素中心即可锐利
- 斜线:在长度方向上跨越无数像素,整体平移 +0.5 只是换一组像素被跨越,不会更清晰 想要斜线相对锐利,通常做法是:
- lineWidth 设为 2(牺牲细度换清晰度)
- 或者干脆接受 1px 斜线的轻微抗锯齿,这是正常的
最终一句话记忆
Canvas 坐标在像素角,1px 线要放在像素中心,所以 +0.5。
需要我给你写一段对比代码,一眼看清模糊 vs 清晰吗?