Canvas 与 SVG
问题
Canvas 和 SVG 是什么?它们有什么区别?各适合什么场景?
答案
Canvas 和 SVG 是浏览器提供的两种主要图形绘制技术。Canvas 是基于像素的即时模式渲染,通过 JavaScript API 在画布上逐像素绘图;SVG 是基于 XML 的保留模式渲染,通过声明式标签描述矢量图形。两者各有优劣,选择取决于具体场景。
1. Canvas 基础
<canvas> 是 HTML5 新增的元素,提供了一块可以通过 JavaScript 进行像素级绘图的画布区域。Canvas 本身只是一个容器,所有绘图操作都通过其渲染上下文(Context)完成。
<!-- 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 的 width 和 height 属性设置的是画布的实际像素大小,而不是 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);
文字绘制
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);
图片绘制
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);
};
变换操作
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() 操作的是一个状态栈,保存的内容包括:变换矩阵、裁剪区域、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 渲染。
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 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="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 对比
| 对比维度 | Canvas | SVG |
|---|---|---|
| 渲染方式 | 位图(像素),即时模式 | 矢量(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 事件,记录路径并绘制
- 截图工具:利用
html2canvas或getDisplayMedia+ Canvas 实现
8. SVG 实际应用
图标系统(symbol + use)
<!-- 定义图标集合(通常放在页面顶部或单独文件中) -->
<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 项目中,通常将 SVG 封装为组件。Vite 可使用 vite-plugin-svgr,Webpack 可使用 @svgr/webpack,将 .svg 文件直接导入为 React 组件:
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 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 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.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,纯粹用于图像处理
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 在现代浏览器中已获得广泛支持(Chrome 69+、Firefox 105+、Safari 16.4+)。使用前建议检测:
const isSupported = typeof OffscreenCanvas !== 'undefined';
常见面试问题
Q1: Canvas 和 SVG 有什么区别?各适合什么场景?
答案:
这是最核心的对比问题,需要从多个维度回答:
| 对比维度 | Canvas | SVG |
|---|---|---|
| 本质 | 位图,基于像素的即时模式渲染 | 矢量图,基于 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);
关键点:
canvas.width/height设置的是画布的像素分辨率canvas.style.width/height设置的是CSS 显示大小- 分辨率放大 dpr 倍 + 显示大小不变 = 用更多像素渲染同样面积 = 高清
ctx.scale(dpr, dpr)确保后续绑定代码中的坐标值仍然基于 CSS 像素
Q3: 如何用 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 等
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)和内部坐标空间之间建立映射关系,从而实现:
- 响应式缩放:内部图形会自动按比例缩放到视口大小
- 裁剪和平移:调整 min-x、min-y 可以显示图形的不同区域
- 等比例布局:无论 SVG 实际显示多大,内部坐标始终一致
<!-- 内部坐标 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
<!-- 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 组件化
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(简单高效)
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。
两种使用方式:
- 转移控制权:将已有
<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 中绑定...
};
- 独立创建:在 Worker 中创建不绑定 DOM 的 Canvas,用于图像处理
// 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 截图生成。