入门:定位、getContext 与高清屏适配
基于 Canvas 2D API(2026 浏览器基线)· 核于 2026-07
速查
- 定位:Canvas 2D = 立即模式位图绘图 API——JS 逐条命令把像素画进位图缓冲,画完即忘、不保留对象;改一处 = 自己清屏重画一帧。
- 立即 vs 保留:SVG/DOM 是保留模式(浏览器持有对象树,改属性自动重渲染、天然有事件);Canvas 内部图形没有 DOM 事件,点击命中、拖拽全要自建模型。
- 选型口径:节点少、交互富、要无障碍 → SVG;上万点散点图 / 实时 K 线 → Canvas 2D(SVG 的 DOM 数量先崩);十万级以上或 3D → WebGL。
- 混合策略:底层海量数据 canvas/WebGL + 顶层少量交互控件 SVG/HTML 叠加(ECharts 默认 canvas 可选 svg;D3 大数据量官方建议切 canvas)。
- 立即模式推论:canvas 无法「选中已画图形」——Excalidraw 类编辑器全是自建场景模型 + 全量/增量重绘。
- 元素基础:
</canvas>结束标签必需;标签内是不支持时的替代内容(也是无障碍出口)。- 不写
width/height属性时默认 300×150。 - 属性宽高 = 位图缓冲尺寸;CSS 宽高 = 显示拉伸——两者比例不一致 → 图像失真/模糊。
- getContext:
canvas.getContext("2d")取 2D 上下文;if (canvas.getContext)做支持性检测。- contextType 可选:
"2d"/"webgl"/"webgl2"/"webgpu"/"bitmaprenderer"。 - 同一 canvas 同类型二次调用返回同一实例;已设为其他模式则返回 null;已
transferControlToOffscreen()再调 getContext 抛InvalidStateError。
- 2d 上下文选项(getContext 第二参数):
alpha(默认 true):设 false 声明背景不透明,可加速绘制。willReadFrequently(默认 false):声明频繁回读像素 → 强制软件渲染,省去 GPU→CPU 回读(2024-09 起 Newly Baseline)。desynchronized:解耦绘制与事件循环降输入延迟,Blink(Chrome/Edge)专属提示。colorSpace:"srgb"(默认)/"display-p3"广色域(约 2023 起三大浏览器可用)。
- 模糊原理:缓冲尺寸 = CSS 尺寸时,1 个画布像素被拉伸到 dpr² 个物理像素 → 必糊。
- 高清屏三步法(
devicePixelRatio= 物理像素 / CSS 像素,Retina 常为 2):- CSS 设显示尺寸(逻辑像素):
canvas.style.width = "200px"。 - 位图缓冲 = 逻辑尺寸 × dpr:
canvas.width = Math.floor(200 * dpr)。 - 坐标系放大:
ctx.scale(dpr, dpr),之后照旧用 CSS 像素坐标绘制。
- CSS 设显示尺寸(逻辑像素):
- 监听 dpr 变化(拖到别的屏 / 浏览器缩放):
matchMedia("(resolution: ...dppx)")的change事件里重跑三步法。 - 事件坐标换算:dpr 适配后缓冲 ≠ CSS 尺寸,
event.offsetX须乘canvas.width / rect.width再用于绘制/拾取。 - 重设 width 的副作用:给
canvas.width赋值(即使同值)= 清空位图 + 重置全部上下文状态(变换/样式全丢)。 - 坐标与角度:原点在左上角,x 向右、y 向下;角度一律弧度——
rad = deg * Math.PI / 180。 - 第一个绘制:
fillRect/strokeRect/clearRect三兄弟直接绘制、不进路径;fillStyle接受 CSS 颜色/渐变/图案,默认黑,一经设置持续生效(状态机)。 - clearRect 语义:把矩形区域清成透明黑。
- 进阶顺序:本页 → 绘图基础 → 图像与像素 → 变换与状态 → 动画 → 性能优化。
一、定位:立即模式 vs 保留模式
Canvas 与 SVG 的本质分野在渲染模型:
- Canvas = 立即模式:调用
fillRect后只剩下像素,浏览器不记得「这里有个矩形」。想让一个圆动起来,只能清屏 → 以新坐标重画;想知道「点击落在哪个图形上」,只能自己维护对象数组做命中检测。 - SVG/DOM = 保留模式:浏览器持有完整对象树,改属性即自动重渲染,每个图形天然可绑事件、可被 CSS 控制、可被无障碍工具读取。
这条差异衍生出选型口径(面试高频):节点少、交互富、要无障碍 → SVG;上万点的散点图、实时刷新的 K 线 → Canvas 2D(SVG 的 DOM 数量先崩);十万级以上图元或 3D → WebGL。业界佐证:ECharts 默认 canvas 渲染、可选 svg;D3 默认操 DOM/SVG、大数据量官方建议切 canvas。混合策略也很常见:底层海量数据走 canvas/WebGL,顶层少量交互控件用 SVG/HTML 叠加。完整对比表见参考页。
二、canvas 元素:宽高属性与替代内容
<!-- 结束标签必需;标签内是不支持时的替代内容(也是无障碍出口) -->
<canvas id="tutorial" width="150" height="150">
<img src="images/clock.png" width="150" height="150" alt="时钟" />
</canvas>两个关键事实:
- 不写
width/height属性时默认 300×150。 - 属性宽高决定位图缓冲尺寸,CSS 宽高只负责显示拉伸。若只用 CSS 把 canvas 撑大,300×150 的缓冲会被拉伸得发虚——MDN 原文:绘制时图像会伸缩以适应框架尺寸。正确姿势是用属性(或 JS)设缓冲尺寸,CSS 仅控显示,或直接走下文的 dpr 三步法。
另一个隐蔽副作用:给 canvas.width 赋值(即使赋同值)会清空位图并重置全部上下文状态——变换、样式、裁剪全丢。有人用 canvas.width = canvas.width 清屏,代价是 dpr 适配的 scale 也随之失效,需要重跑初始化;常规清屏应该用 clearRect 或 ctx.reset()(见动画页)。
三、getContext 与上下文选项
const canvas = document.getElementById("tutorial");
if (canvas.getContext) {
// 支持性检测:老浏览器没有 getContext 方法
const ctx = canvas.getContext("2d");
}- contextType:
"2d"、"webgl"、"webgl2"、"webgpu"、"bitmaprenderer"。 - 同一 canvas 同类型二次调用返回同一实例;如果已设置为其他模式(比如先取了 webgl 再要 2d),返回 null。
- 已经
transferControlToOffscreen()移交控制权的 canvas,再调 getContext 抛InvalidStateError。
2d 上下文还接受第二个参数(上下文属性对象),四个选项都值得知道:
| 选项 | 默认 | 作用 |
|---|---|---|
alpha | true | 设 false 声明背景不透明,跳过 alpha 合成、加速绘制 |
willReadFrequently | false | 声明将频繁回读像素 → 强制软件渲染(放弃硬件加速),高频 getImageData 时省去 GPU→CPU 回读反而更快(2024-09 起 Newly Baseline,Safari 18 补齐) |
desynchronized | — | 提示解耦画布绘制与事件循环以降低输入延迟(手写笔场景);Blink 专属提示,其他引擎忽略 |
colorSpace | "srgb" | 可设 "display-p3" 广色域(Chrome 94+ / Safari 15.4+ / Firefox 111+,约 2023 起可用) |
// 典型:取色器/视频逐帧处理这类高频回读场景
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// 典型:不透明背景的游戏画布
const ctx2 = canvas2.getContext("2d", { alpha: false });四、高清屏适配:devicePixelRatio 三步法(必考)
window.devicePixelRatio = 物理像素 / CSS 像素(Retina 常为 2)。若位图缓冲尺寸 = CSS 尺寸,1 个画布像素会被拉伸到 dpr² 个物理像素 → 模糊。修正三步:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const size = 200; // 期望的 CSS 逻辑尺寸
// ① CSS 显示尺寸(逻辑像素)
canvas.style.width = `${size}px`;
canvas.style.height = `${size}px`;
// ② 位图缓冲尺寸 = 逻辑尺寸 × dpr(物理像素)
const dpr = window.devicePixelRatio;
canvas.width = Math.floor(size * dpr);
canvas.height = Math.floor(size * dpr);
// ③ 坐标系放大 dpr 倍,后续绘制照旧用 CSS 像素坐标
ctx.scale(dpr, dpr);补充两点:
- 监听 dpr 变化(窗口拖到别的屏 / 浏览器缩放):
matchMedia(`(resolution: ${devicePixelRatio}dppx)`).addEventListener(
"change",
update, // dpr 变了重跑三步法
);- 事件坐标要换算:dpr 适配后缓冲尺寸 ≠ CSS 尺寸,
event.offsetX直接喂给绘制/拾取会错位,须乘canvas.width / rect.width(详见动画页的拾取一节)。
五、第一个绘制
矩形三件套直接绘制、不进路径,是最快看到像素的方式:
const canvas = document.getElementById("tutorial");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#09F"; // CSS 颜色字符串,默认 #000;设置后对后续绘制持续生效
ctx.fillRect(10, 10, 100, 100); // 填充矩形 (x, y, w, h)
ctx.strokeStyle = "red";
ctx.strokeRect(130, 10, 100, 100); // 描边矩形
ctx.clearRect(40, 40, 40, 40); // 清成透明黑(在填充矩形上挖个洞)三个立刻能体会到的「Canvas 脾气」:
- 坐标原点在左上角,x 向右、y 向下。
- 角度一律用弧度:
arc/rotate传 90 期望直角,实际是 90 弧度 ≈ 14.3 圈——rad = deg * Math.PI / 180。 fillStyle是状态不是参数:一经设置对后续所有绘制持续生效,这就是「上下文是状态机」的第一次照面。
矩形之外的一切图形(三角形、圆、曲线)都要走路径系统:beginPath() → 画图命令 → closePath()(可选)→ stroke()/fill()。下一页绘图基础从路径系统讲起。