Canvas 基础:坐标系、像素与 2D 绘制上下文

<canvas> 是前端里最基础也最容易被忽略的一块:看起来只是一个能画图的标签,但背后有一整套关于坐标系、像素密度和状态管理的规则。
这一篇从三个问题入手:Canvas 在页面中的角色是什么、默认坐标系和像素是怎么定义的,以及如何用 2D 上下文的基础 API 画出第一批图形。

在 DOM 里,Canvas 只是一个普通元素:

  • 通过 widthheight 属性决定画布的逻辑尺寸(以像素为单位);
  • 通过 CSS 再决定它在页面上的展示尺寸(可能被缩放)。

想在上面画东西,需要获取「绘制上下文」:

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

这里的 ctx 就是 Canvas 2D API 的入口,后续所有画线、填充、写字、绘图等操作都通过它来完成。

可以把结构简单理解为:

  • <canvas>:一块「像素缓冲区」;
  • ctx(2D 上下文):一支「画笔」,提供一整套绘图命令。

在 2D 上下文中,默认坐标系是:

  • 原点 (0, 0) 在画布左上角;
  • x 轴向右增大;
  • y 轴向下增大。

所有绘图 API(例如 fillRect(x, y, width, height))的参数都基于这个默认坐标系:

  • xy 是矩形左上角的位置;
  • widthheight 是宽高。

Canvas API 的坐标和尺寸参数都是浮点数,但最终渲染时需要映射到离散像素上:

  • 在非整数坐标绘制 1 像素线条时,可能会出现「模糊」或「半像素」的效果;
  • 通常建议在需要精确 1 像素线条时,注意坐标的对齐(例如 0.5 偏移的用法)。

理解这一点,在后续做高清适配或细节绘制时会更顺手。

Canvas 的逻辑尺寸(canvas.width / canvas.height)和 CSS 尺寸(style.width / style.height)在高 DPI 设备上经常不一致:

  • CSS 上看是 300×150;
  • 实际像素可能是两倍(例如 Retina 屏上常见的 600×300)。

如果不做适配,1 逻辑像素对应多物理像素,会导致:

  • 所有内容被浏览器拉伸,看起来发糊。

典型做法是:

  • 将 Canvas 的实际宽高设为 CSS 宽高乘以 window.devicePixelRatio
  • 再通过 scale 把 2D 上下文坐标系调回到逻辑尺寸。

示例思路如下:

 1const canvas = document.getElementById("canvas");
 2const ctx = canvas.getContext("2d");
 3
 4const dpr = window.devicePixelRatio || 1;
 5const rect = canvas.getBoundingClientRect();
 6
 7canvas.width = rect.width * dpr;
 8canvas.height = rect.height * dpr;
 9
10ctx.scale(dpr, dpr);

这样即便在高 DPI 设备上,1 个逻辑单位仍对应视觉上的 1 像素,但背后有更多物理像素用于绘制,从而保持清晰。

Canvas 2D API 提供了几组基础绘图命令,可以先掌握三类:

  • 直接绘制矩形;
  • 使用路径绘制任意形状;
  • 设置填充与描边样式。

矩形是最简单的图形,常用三个方法:

  • fillRect(x, y, width, height):绘制填充矩形;
  • strokeRect(x, y, width, height):绘制描边矩形;
  • clearRect(x, y, width, height):清除一个区域(使其变透明)。

之前设置的 fillStylestrokeStyle 会影响填充和描边颜色。

更复杂的图形需要用路径来构建:

  • beginPath():开始一条新路径;
  • moveTo(x, y):把画笔移动到某个点(不画线);
  • lineTo(x, y):从当前点画一条线到指定位置;
  • arc(...) / rect(...) / quadraticCurveTo(...) / bezierCurveTo(...):画圆弧、矩形、曲线等;
  • closePath():闭合路径;
  • fill():填充路径内部;
  • stroke():沿路径描边。

路径的核心是:

  • 先用一组命令构造出「轮廓」;
  • 然后选择填充或描边方式进行输出。

常用的样式属性包括:

  • fillStyle:填充颜色,可以是字符串(如 '#ff0000''rgba(...)')、渐变或图案;
  • strokeStyle:描边颜色;
  • lineWidth:线宽;
  • lineJoinlineCap:线连接方式与端点样式。

这些属性会作用于后续的 fill() / stroke() / fillRect() / strokeRect() 调用,直到被修改或状态被恢复。

Canvas 上下文内部有一整套状态:

  • 当前变换矩阵(平移、旋转、缩放);
  • 填充/描边样式;
  • 线条参数、阴影、全局透明度等。

当调用绘图 API 时,都会使用当前状态:

  • 先设置状态,再调用绘图命令;
  • 状态会一直保留,除非显式修改或通过 save() / restore() 管理。

在简单场景里可以不太在意这点,但在稍复杂的绘制中,良好的状态管理能避免很多「为什么后面的线条颜色也变了」之类的问题。

这一篇可以压缩成几条关键记忆点:

  • <canvas> + 2d 上下文的关系:一个是像素缓冲区,一个是绘图 API;
  • 默认坐标系原点在左上角,x 向右、y 向下,坐标与尺寸都是浮点数;
  • 逻辑尺寸与 CSS 尺寸配合 devicePixelRatio 决定了绘制清晰度;
  • 最基础的绘图能力来自矩形和路径 API,配合样式属性与状态概念使用。

在这些基础之上,后续再看变换矩阵、动画、交互和性能优化时,就不会在「坐标和像素到底怎么回事」上纠缠。