跳到主要内容

Canvas 与 SVG

问题

Canvas 和 SVG 是什么?它们有什么区别?各适合什么场景?

答案

Canvas 和 SVG 是浏览器提供的两种主要图形绘制技术。Canvas 是基于像素的即时模式渲染,通过 JavaScript API 在画布上逐像素绘图;SVG 是基于 XML 的保留模式渲染,通过声明式标签描述矢量图形。两者各有优劣,选择取决于具体场景。

1. Canvas 基础

<canvas> 是 HTML5 新增的元素,提供了一块可以通过 JavaScript 进行像素级绘图的画布区域。Canvas 本身只是一个容器,所有绘图操作都通过其渲染上下文(Context)完成。

Canvas 基本结构
<!-- canvas 默认宽 300px,高 150px -->
<canvas id="myCanvas" width="600" height="400">
您的浏览器不支持 Canvas
</canvas>
获取绘图上下文
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;

// 2D 上下文 —— 最常用
const ctx = canvas.getContext('2d')!;

// WebGL 上下文 —— 3D 渲染
const gl = canvas.getContext('webgl')!;

// WebGL 2 上下文
const gl2 = canvas.getContext('webgl2')!;
注意

Canvas 的 widthheight 属性设置的是画布的实际像素大小,而不是 CSS 显示大小。如果只用 CSS 设置宽高而不设置属性,画布会被拉伸导致模糊。

Canvas 坐标系:原点 (0, 0) 在画布左上角,x 轴向右递增,y 轴向下递增。

2. Canvas 2D API

路径绘制

Canvas 绘图的核心是路径(Path)。通过一系列路径命令描述图形,然后统一填充或描边。

路径绘制基础
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

ctx.beginPath(); // 开始新路径(重要!不调用会与上一次路径混在一起)
ctx.moveTo(50, 50); // 移动画笔到起点
ctx.lineTo(200, 50); // 画直线到目标点
ctx.lineTo(200, 150); // 继续画直线
ctx.closePath(); // 闭合路径(连接终点和起点)

ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke(); // 描边

ctx.fillStyle = 'rgba(66, 133, 244, 0.3)';
ctx.fill(); // 填充
绘制圆弧与圆形
const ctx = canvas.getContext('2d')!;

// 圆弧:arc(x, y, radius, startAngle, endAngle, anticlockwise?)
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2); // 完整圆形
ctx.fillStyle = '#4285f4';
ctx.fill();

// 扇形
ctx.beginPath();
ctx.moveTo(400, 150);
ctx.arc(400, 150, 80, 0, Math.PI * 0.75);
ctx.closePath();
ctx.fillStyle = '#ea4335';
ctx.fill();

填充与描边

渐变与阴影
const ctx = canvas.getContext('2d')!;

// 线性渐变
const gradient = ctx.createLinearGradient(0, 0, 300, 0);
gradient.addColorStop(0, '#667eea');
gradient.addColorStop(1, '#764ba2');
ctx.fillStyle = gradient;
ctx.fillRect(20, 20, 300, 100);

// 阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#34a853';
ctx.fillRect(20, 150, 200, 80);

文字绘制

Canvas 文字
const ctx = canvas.getContext('2d')!;

ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center'; // left | right | center | start | end
ctx.textBaseline = 'middle'; // top | hanging | middle | alphabetic | bottom

// 填充文字
ctx.fillStyle = '#333';
ctx.fillText('Hello Canvas', 300, 50);

// 描边文字
ctx.strokeStyle = '#4285f4';
ctx.lineWidth = 1;
ctx.strokeText('Stroke Text', 300, 100);

// 测量文字宽度
const metrics: TextMetrics = ctx.measureText('Hello Canvas');
console.log('文字宽度:', metrics.width);

图片绘制

drawImage 三种用法
const ctx = canvas.getContext('2d')!;
const img = new Image();
img.src = 'photo.jpg';

img.onload = () => {
// 1. 基础:drawImage(image, dx, dy)
ctx.drawImage(img, 0, 0);

// 2. 缩放:drawImage(image, dx, dy, dWidth, dHeight)
ctx.drawImage(img, 0, 0, 200, 150);

// 3. 裁剪+缩放:drawImage(image, sx, sy, sW, sH, dx, dy, dW, dH)
// 从源图 (100,100) 处裁剪 200x200,绘制到画布 (0,0) 缩放为 150x150
ctx.drawImage(img, 100, 100, 200, 200, 0, 0, 150, 150);
};

变换操作

save/restore 与变换
const ctx = canvas.getContext('2d')!;

ctx.save(); // 保存当前绘图状态到栈中

ctx.translate(200, 200); // 移动原点
ctx.rotate(Math.PI / 4); // 旋转 45 度
ctx.scale(1.5, 1.5); // 缩放 1.5 倍

ctx.fillStyle = '#ea4335';
ctx.fillRect(-50, -50, 100, 100); // 以新原点为中心画矩形

ctx.restore(); // 恢复之前保存的状态(变换、样式等全部恢复)

// 此时坐标系回到 save 之前的状态
ctx.fillStyle = '#4285f4';
ctx.fillRect(10, 10, 50, 50);
save/restore 要点

save()restore() 操作的是一个状态栈,保存的内容包括:变换矩阵、裁剪区域、fillStyle、strokeStyle、font、globalAlpha、lineWidth、lineCap、lineJoin、shadowColor 等。可以嵌套多次调用。

3. Canvas 动画

Canvas 动画的核心思路是:清空画布 -> 更新状态 -> 重新绘制 -> 循环。配合 requestAnimationFrame 实现流畅的 60fps 动画。

弹跳球动画
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

canvas.width = 600;
canvas.height = 400;

// 球的状态
interface Ball {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
color: string;
}

const ball: Ball = {
x: 100,
y: 100,
vx: 4,
vy: 3,
radius: 20,
color: '#4285f4',
};

function update(ball: Ball): void {
ball.x += ball.vx;
ball.y += ball.vy;

// 碰到边界反弹
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.vx = -ball.vx;
}
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.vy = -ball.vy;
}
}

function draw(ball: Ball): void {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
}

function animate(): void {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 1. 清空画布
update(ball); // 2. 更新状态
draw(ball); // 3. 重新绘制
requestAnimationFrame(animate); // 4. 循环
}

animate();

更多动画性能优化技巧可参考 动画性能优化

4. Canvas 高清适配

在 Retina(高 DPI)屏幕上,如果不做处理,Canvas 内容会显得模糊。这是因为 1 个 CSS 像素对应多个物理像素,而 Canvas 默认按 1:1 渲染。

高清 Canvas 设置
function setupHiDPICanvas(
canvas: HTMLCanvasElement,
width: number,
height: number
): CanvasRenderingContext2D {
const dpr = window.devicePixelRatio || 1;

// 设置 canvas 实际像素大小(放大 dpr 倍)
canvas.width = width * dpr;
canvas.height = height * dpr;

// CSS 显示大小保持不变
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;

const ctx = canvas.getContext('2d')!;
// 缩放绘图上下文,使后续绘图代码无需手动乘以 dpr
ctx.scale(dpr, dpr);

return ctx;
}

// 使用
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = setupHiDPICanvas(canvas, 600, 400);

// 后续绘图代码与普通 Canvas 完全一致
ctx.fillStyle = '#4285f4';
ctx.fillRect(10, 10, 100, 100);
原理说明
  • canvas.width/height:画布的实际像素数,决定绘图分辨率
  • canvas.style.width/height:CSS 显示尺寸,决定画布在页面上占多大
  • 设置 canvas.width = CSS宽 * dpr 后,画布像素数增加,但 CSS 尺寸不变,相当于用更多像素去渲染同样大小的区域,从而实现高清
  • ctx.scale(dpr, dpr) 是为了让绘图坐标与 CSS 像素对齐,避免手动乘以 dpr

设计 H5 海报生成系统 中也详细讨论了高清 Canvas 的应用。

5. SVG 基础

SVG(Scalable Vector Graphics)是一种基于 XML 的矢量图形格式,可以直接嵌入 HTML 中。SVG 中的每个图形都是一个 DOM 元素,可以被 CSS 样式化、被 JavaScript 操作、被事件监听。

基本图形元素

SVG 基本图形
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- 矩形 -->
<rect x="10" y="10" width="100" height="60" rx="10" ry="10"
fill="#4285f4" stroke="#333" stroke-width="2" />

<!-- 圆形 -->
<circle cx="220" cy="40" r="30" fill="#ea4335" />

<!-- 椭圆 -->
<ellipse cx="350" cy="40" rx="60" ry="30" fill="#fbbc05" />

<!-- 直线 -->
<line x1="10" y1="120" x2="200" y2="120"
stroke="#34a853" stroke-width="3" />

<!-- 折线 -->
<polyline points="220,100 250,140 280,110 310,150"
fill="none" stroke="#4285f4" stroke-width="2" />

<!-- 多边形 -->
<polygon points="400,100 430,150 370,150"
fill="#ea4335" stroke="#333" stroke-width="1" />

<!-- 路径 —— 最强大的元素 -->
<path d="M 10 200 Q 100 250 200 200 T 400 200"
fill="none" stroke="#764ba2" stroke-width="3" />

<!-- 文字 -->
<text x="10" y="280" font-size="24" fill="#333">SVG Text</text>
</svg>

path 命令速查

命令含义示例
M x y移动到 (moveTo)M 10 20
L x y直线到 (lineTo)L 100 200
H x水平线到H 100
V y垂直线到V 200
C x1 y1 x2 y2 x y三次贝塞尔曲线C 20 20 40 20 50 10
Q x1 y1 x y二次贝塞尔曲线Q 30 50 50 10
A rx ry rotation large-arc sweep x y椭圆弧A 25 25 0 0 1 50 25
Z闭合路径Z
小写命令

大写表示绝对坐标,小写表示相对坐标。例如 l 10 20 表示从当前点向右 10、向下 20。

viewBox 属性

viewBox 定义了 SVG 的内部坐标系统,实现响应式缩放。

viewBox 实现响应式
<!-- viewBox="minX minY width height" -->
<!-- 无论 SVG 实际显示多大,内部坐标始终是 0~100 -->
<svg viewBox="0 0 100 100" width="300" height="300">
<circle cx="50" cy="50" r="40" fill="#4285f4" />
</svg>

<!-- 响应式:宽度 100%,高度自动按比例 -->
<svg viewBox="0 0 100 50" style="width: 100%; height: auto;">
<rect x="10" y="10" width="80" height="30" fill="#ea4335" />
</svg>

内联 SVG vs <img> 引用

方式优点缺点
内联 <svg>可 CSS 样式化、JS 操控、无额外请求增大 HTML 体积、不可缓存
<img src="icon.svg">可缓存、代码简洁不可 CSS 修改内部样式、不可 JS 操控
CSS background-image可缓存、方便控制大小不可修改颜色、不可交互
<object> / <embed>保留 SVG 交互性额外请求、兼容性考虑

6. SVG vs Canvas 对比

对比维度CanvasSVG
渲染方式位图(像素),即时模式矢量(XML),保留模式
DOM 元素只有一个 <canvas> 元素每个图形都是 DOM 元素
事件处理只能监听整个画布,需手动计算命中每个元素可独立绑定事件
缩放放大会模糊(需重绘)无限缩放不失真
大量元素性能优秀(不受元素数量影响)差(DOM 节点过多导致卡顿)
少量元素性能一般(需要 JS 循环重绘)优秀(浏览器自动管理)
动画JS 手动逐帧重绘CSS/SMIL/JS 均可,声明式
可访问性差(纯像素,需额外 ARIA)好(DOM 结构天然支持辅助技术)
SEO不友好友好(文本可被搜索引擎索引)
文件大小不适用(动态生成)复杂图形 XML 较大
学习曲线需掌握绑定 API声明式标签,门槛较低
选择建议
  • 选 Canvas:游戏、大量粒子/数据点、图片处理、实时绘图、海报生成
  • 选 SVG:图标系统、数据可视化(少量元素)、Logo、UI 图形、需要交互和动画的图形
  • 混合使用:很多可视化库(如 ECharts)支持 Canvas 和 SVG 两种渲染器,小数据量用 SVG,大数据量切 Canvas

7. Canvas 实际应用

图片编辑器(裁剪/滤镜)

Canvas 是图片编辑器的核心技术,详细的系统设计可参考 设计在线图片编辑器

图片裁剪
function cropImage(
img: HTMLImageElement,
sx: number, sy: number,
sWidth: number, sHeight: number
): string {
const canvas = document.createElement('canvas');
canvas.width = sWidth;
canvas.height = sHeight;
const ctx = canvas.getContext('2d')!;

// 利用 drawImage 的 9 参数版本进行裁剪
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);

return canvas.toDataURL('image/png');
}
像素级滤镜操作
function applyGrayscale(ctx: CanvasRenderingContext2D, width: number, height: number): void {
const imageData: ImageData = ctx.getImageData(0, 0, width, height);
const data: Uint8ClampedArray = imageData.data;

// 每个像素占 4 个字节:R, G, B, A
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
// data[i + 3] 是 Alpha,保持不变
}

ctx.putImageData(imageData, 0, 0);
}

// 也可以用 CSS filter(性能更好,但灵活性不如像素操作)
ctx.filter = 'grayscale(100%) blur(2px)';
ctx.drawImage(img, 0, 0);

数据可视化

简单柱状图
interface ChartData {
label: string;
value: number;
color: string;
}

function drawBarChart(
ctx: CanvasRenderingContext2D,
data: ChartData[],
width: number,
height: number
): void {
const padding = 40;
const chartWidth = width - padding * 2;
const chartHeight = height - padding * 2;
const barWidth = chartWidth / data.length * 0.6;
const gap = chartWidth / data.length * 0.4;
const maxValue = Math.max(...data.map(d => d.value));

data.forEach((item, i) => {
const barHeight = (item.value / maxValue) * chartHeight;
const x = padding + i * (barWidth + gap);
const y = height - padding - barHeight;

ctx.fillStyle = item.color;
ctx.fillRect(x, y, barWidth, barHeight);

// 标签
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + barWidth / 2, height - 15);
ctx.fillText(String(item.value), x + barWidth / 2, y - 8);
});
}

其他应用

  • 海报生成:利用 html2canvas 将 DOM 转为 Canvas,再导出为图片,详见 设计 H5 海报生成系统
  • 游戏开发:2D 游戏引擎(Phaser、PixiJS)底层都基于 Canvas/WebGL
  • 签名板:监听 touch/mouse 事件,记录路径并绘制
  • 截图工具:利用 html2canvasgetDisplayMedia + Canvas 实现

8. SVG 实际应用

图标系统(symbol + use)

SVG Sprite 图标系统
<!-- 定义图标集合(通常放在页面顶部或单独文件中) -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1" fill="none" stroke="currentColor" stroke-width="2"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" fill="none" stroke="currentColor" stroke-width="2"/>
</symbol>
</svg>

<!-- 使用图标 -->
<svg class="icon" width="24" height="24">
<use href="#icon-home" />
</svg>

<svg class="icon" width="32" height="32" style="color: #4285f4;">
<use href="#icon-search" />
</svg>
图标样式
.icon {
/* 使用 currentColor 让图标颜色继承父元素 */
fill: currentColor;
stroke: currentColor;
/* 方便统一控制大小 */
width: 1em;
height: 1em;
}
React 组件化图标

在 React 项目中,通常将 SVG 封装为组件。Vite 可使用 vite-plugin-svgr,Webpack 可使用 @svgr/webpack,将 .svg 文件直接导入为 React 组件:

React SVG 图标组件
import { SVGProps } from 'react';

// 通过构建工具自动转换
import HomeSvg from './icons/home.svg?react';

// 或手动封装
function IconHome(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" {...props}>
<path d="M3 12l2-2m0 0l7-7 7 7..." strokeWidth={2} />
</svg>
);
}

数据可视化(D3.js)

SVG 是 D3.js 的默认渲染方式,适合中小规模数据的交互式可视化。

SVG 动画

SVG CSS 动画
<svg width="200" height="200">
<circle cx="100" cy="100" r="40" fill="#4285f4" class="pulse" />
</svg>

<style>
.pulse {
animation: pulse 2s ease-in-out infinite;
transform-origin: center;
}
@keyframes pulse {
0%, 100% { r: 40; opacity: 1; }
50% { r: 55; opacity: 0.6; }
}
</style>
SVG SMIL 动画(内置)
<svg width="200" height="200">
<rect x="10" y="80" width="40" height="40" fill="#ea4335">
<!-- 平移动画 -->
<animateTransform
attributeName="transform"
type="translate"
values="0,0; 140,0; 0,0"
dur="3s"
repeatCount="indefinite" />
</rect>
</svg>

9. OffscreenCanvas

OffscreenCanvas 允许在 Web Worker 中进行 Canvas 渲染,不阻塞主线程,适合计算密集型的绘图任务。

主线程:转交控制权
// main.ts
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;

const offscreen: OffscreenCanvas = canvas.transferControlToOffscreen();

const worker = new Worker('worker.ts');
worker.postMessage({ canvas: offscreen }, [offscreen]); // 转移所有权
Worker 线程:执行绑定
// worker.ts
self.onmessage = (e: MessageEvent<{ canvas: OffscreenCanvas }>) => {
const canvas = e.data.canvas;
const ctx = canvas.getContext('2d')!;

// 在 Worker 中绑定,不阻塞主线程
function render(): void {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#4285f4';
ctx.fillRect(
Math.random() * canvas.width,
Math.random() * canvas.height,
50, 50
);
requestAnimationFrame(render);
}

render();
};
不绑定 DOM 的 OffscreenCanvas(纯计算)
// 也可以不绑定 DOM,纯粹用于图像处理
const worker = new Worker('image-worker.ts');

// image-worker.ts
self.onmessage = async (e: MessageEvent<{ width: number; height: number }>) => {
const { width, height } = e.data;
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d')!;

// 执行耗时的绘图操作...
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, width, height);

// 转为 Blob 发回主线程
const blob = await offscreen.convertToBlob({ type: 'image/png' });
self.postMessage({ blob });
};
OffscreenCanvas 兼容性

OffscreenCanvas 在现代浏览器中已获得广泛支持(Chrome 69+、Firefox 105+、Safari 16.4+)。使用前建议检测:

const isSupported = typeof OffscreenCanvas !== 'undefined';

常见面试问题

Q1: Canvas 和 SVG 有什么区别?各适合什么场景?

答案

这是最核心的对比问题,需要从多个维度回答:

对比维度CanvasSVG
本质位图,基于像素的即时模式渲染矢量图,基于 XML 的保留模式渲染
DOM整个画布只有一个 <canvas> DOM 元素每个图形都是独立的 DOM 元素
事件只能对整个画布监听,命中检测需手动计算每个元素可直接绑定事件监听器
缩放放大后模糊(像素化)无限缩放不失真
大量元素性能优秀,与元素数量关系不大性能差,DOM 节点过多导致卡顿
动画JS 手动 requestAnimationFrame 逐帧重绘支持 CSS 动画、SMIL、JS 多种方式
可访问性差,纯像素无语义好,有 DOM 结构可被辅助技术读取
修改方式需要重绘整个画布(或部分区域)直接修改某个元素的属性即可

适用场景

  • Canvas:游戏(大量精灵/粒子)、图片编辑器、海报生成、数据可视化(万级数据点以上)、签名板、视频处理
  • SVG:图标系统、Logo、数据可视化(几百个元素以内的交互图表)、动画图形、地图、需要缩放的 UI 元素

Q2: Canvas 如何实现高清屏适配?

答案

高清屏(Retina)上 1 个 CSS 像素对应多个物理像素。window.devicePixelRatio(简称 dpr)表示这个倍率关系。

核心思路是让 Canvas 的实际像素比 CSS 显示尺寸大 dpr 倍:

高清适配方案
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const dpr = window.devicePixelRatio || 1;

// 期望的 CSS 显示大小
const cssWidth = 300;
const cssHeight = 200;

// 1. canvas 实际像素 = CSS尺寸 * dpr
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;

// 2. CSS 显示尺寸不变
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;

// 3. 缩放 context,让绘图坐标与 CSS 像素对齐
const ctx = canvas.getContext('2d')!;
ctx.scale(dpr, dpr);

关键点

  1. canvas.width/height 设置的是画布的像素分辨率
  2. canvas.style.width/height 设置的是CSS 显示大小
  3. 分辨率放大 dpr 倍 + 显示大小不变 = 用更多像素渲染同样面积 = 高清
  4. ctx.scale(dpr, dpr) 确保后续绑定代码中的坐标值仍然基于 CSS 像素

Q3: 如何用 Canvas 实现一个简单的动画?

答案

Canvas 动画的核心是 "清空 -> 更新 -> 绘制 -> 循环" 四步。

Canvas 动画框架
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

// 状态
let x = 0;
const speed = 2;

function animate(): void {
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 2. 更新状态
x += speed;
if (x > canvas.width) x = 0; // 循环

// 3. 绘制
ctx.beginPath();
ctx.arc(x, canvas.height / 2, 20, 0, Math.PI * 2);
ctx.fillStyle = '#4285f4';
ctx.fill();

// 4. 下一帧
requestAnimationFrame(animate);
}

animate();

要点

  • 使用 requestAnimationFrame 而非 setInterval/setTimeout,浏览器会在下一次重绘前调用回调,通常为 60fps
  • 每帧必须先 clearRect 清空画布,否则之前绘制的内容会残留
  • 复杂动画可以用时间差(deltaTime)而非固定速度,以应对帧率波动

Q4: Canvas 的 save() 和 restore() 有什么作用?

答案

save()restore() 操作的是 Canvas 的绘图状态栈

  • save():将当前绘图状态压入栈中
  • restore():从栈中弹出最近一次保存的状态并恢复

保存的状态包括:

  • 变换矩阵(translate、rotate、scale)
  • 裁剪区域(clip)
  • 样式属性:fillStyle、strokeStyle、globalAlpha、lineWidth、lineCap、lineJoin、font、textAlign、textBaseline、shadowColor、shadowBlur 等
save/restore 示例
const ctx = canvas.getContext('2d')!;

ctx.fillStyle = 'red';
ctx.save(); // 保存状态 A(fillStyle: red)

ctx.fillStyle = 'blue';
ctx.translate(100, 100);
ctx.save(); // 保存状态 B(fillStyle: blue, translate 100,100)

ctx.fillStyle = 'green';
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 50, 50); // 绿色、旋转 45 度

ctx.restore(); // 恢复到状态 B —— blue、translate(100,100)、无旋转
ctx.fillRect(0, 0, 50, 50); // 蓝色、无旋转

ctx.restore(); // 恢复到状态 A —— red、无位移
ctx.fillRect(0, 0, 50, 50); // 红色、原始位置

典型应用场景:绘制复杂图形时,需要对某个子图形做独立的变换(旋转、缩放),操作完后恢复状态,避免影响后续绘制。

Q5: SVG 的 viewBox 属性是什么?有什么用?

答案

viewBox 定义了 SVG 的内部坐标系,格式为 viewBox="min-x min-y width height"

它的作用是在 SVG 的视口(viewport)内部坐标空间之间建立映射关系,从而实现:

  1. 响应式缩放:内部图形会自动按比例缩放到视口大小
  2. 裁剪和平移:调整 min-x、min-y 可以显示图形的不同区域
  3. 等比例布局:无论 SVG 实际显示多大,内部坐标始终一致
viewBox 示例
<!-- 内部坐标 0~100,实际显示 300x300 -->
<!-- 圆心在 (50,50) 半径 40,无论 SVG 实际多大,圆始终居中 -->
<svg viewBox="0 0 100 100" width="300" height="300">
<circle cx="50" cy="50" r="40" fill="#4285f4" />
</svg>

<!-- 同样的 viewBox,显示 600x600,图形自动放大,不失真 -->
<svg viewBox="0 0 100 100" width="600" height="600">
<circle cx="50" cy="50" r="40" fill="#4285f4" />
</svg>

配合 preserveAspectRatio 属性可以控制缩放行为(类似 CSS 的 object-fit):

<!-- 保持比例,居中对齐 -->
<svg viewBox="0 0 100 50" preserveAspectRatio="xMidYMid meet" width="300" height="300">
...
</svg>

<!-- 不保持比例,拉伸填满 -->
<svg viewBox="0 0 100 50" preserveAspectRatio="none" width="300" height="300">
...
</svg>

Q6: 如何用 SVG 实现图标系统?

答案

主流方案是使用 SVG symbol + use 构建 Sprite 图标系统:

方案一:内联 SVG Sprite

HTML 中定义 + 使用
<!-- 1. 定义 Sprite(隐藏) -->
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M3 12l9-9 9 9M5 10v10h14V10" stroke="currentColor" fill="none" stroke-width="2"/>
</symbol>
<symbol id="icon-user" viewBox="0 0 24 24">
<circle cx="12" cy="8" r="4" stroke="currentColor" fill="none" stroke-width="2"/>
<path d="M4 20c0-4 4-7 8-7s8 3 8 7" stroke="currentColor" fill="none" stroke-width="2"/>
</symbol>
</svg>

<!-- 2. 使用 -->
<svg width="24" height="24"><use href="#icon-home" /></svg>
<svg width="24" height="24"><use href="#icon-user" /></svg>

方案二:React 组件化

React 图标组件
import { SVGProps } from 'react';

interface IconProps extends SVGProps<SVGSVGElement> {
size?: number;
}

function IconHome({ size = 24, ...props }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
{...props}
>
<path d="M3 12l9-9 9 9M5 10v10h14V10" />
</svg>
);
}

方案三:外部 SVG 文件引用

<!-- 引用外部 sprite 文件 -->
<svg width="24" height="24">
<use href="/icons/sprite.svg#icon-home" />
</svg>
注意

外部 SVG 文件引用时,currentColor 不生效,因为跨文档引用不继承 CSS 变量。建议使用内联方式或构建工具处理。

Q7: Canvas 如何实现图片裁剪/滤镜?

答案

裁剪使用 drawImage 的 9 参数版本:

图片裁剪
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// sx,sy,sWidth,sHeight: 源图片的裁剪区域
// dx,dy,dWidth,dHeight: 目标画布的绘制区域

function cropImage(
img: HTMLImageElement,
cropX: number, cropY: number,
cropW: number, cropH: number
): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = cropW;
canvas.height = cropH;
const ctx = canvas.getContext('2d')!;

ctx.drawImage(img, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);

return canvas;
}

滤镜有两种方式:

方式一:CSS filter(简单高效)

CSS filter
const ctx = canvas.getContext('2d')!;
ctx.filter = 'blur(5px) brightness(1.2) contrast(1.1) grayscale(50%)';
ctx.drawImage(img, 0, 0);
ctx.filter = 'none'; // 恢复

方式二:getImageData 像素操作(灵活但慢)

像素级操作
function invertColors(ctx: CanvasRenderingContext2D, w: number, h: number): void {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]

for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R 反转
data[i + 1] = 255 - data[i + 1]; // G 反转
data[i + 2] = 255 - data[i + 2]; // B 反转
// Alpha 不变
}

ctx.putImageData(imageData, 0, 0);
}
性能对比
  • CSS filter:GPU 加速,性能好,但滤镜种类有限
  • getImageData:逐像素操作,完全灵活,但在主线程执行会阻塞 UI。大图建议使用 OffscreenCanvas 在 Worker 中处理

Q8: OffscreenCanvas 是什么?有什么优势?

答案

OffscreenCanvas 是一种可以脱离 DOM 的 Canvas,支持在 Web Worker 中使用,从而将耗时的绑定操作从主线程移到 Worker 线程,避免阻塞 UI。

两种使用方式

  1. 转移控制权:将已有 <canvas> 的渲染控制权转移给 Worker
转移控制权
// 主线程
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// Worker 线程
self.onmessage = (e: MessageEvent) => {
const canvas = e.data.canvas as OffscreenCanvas;
const ctx = canvas.getContext('2d')!;
// 在 Worker 中绑定...
};
  1. 独立创建:在 Worker 中创建不绑定 DOM 的 Canvas,用于图像处理
Worker 中独立使用
// Worker 线程
const offscreen = new OffscreenCanvas(800, 600);
const ctx = offscreen.getContext('2d')!;

// 绘图处理...
ctx.fillRect(0, 0, 800, 600);

// 转为 Blob 发回主线程
const blob = await offscreen.convertToBlob({ type: 'image/png' });
self.postMessage(blob);

优势

  • 不阻塞主线程:复杂绘图和图像处理不会导致 UI 卡顿
  • 更好的动画性能:即使主线程繁忙,Worker 中的动画仍可保持流畅
  • 并行计算:利用多核 CPU 并行处理多个 Canvas 任务

典型应用:图片滤镜处理、复杂图表渲染、游戏物理计算、Canvas 截图生成。

相关链接