
你如果是以前端面试 公司实际开发为目标学 Canvas我建议不要只学“画矩形、画圆”而是按基础 API → 图片处理 → 交互 → 动画 → 性能 → 实际业务场景 → 工程化 → 面试高频问题这条路线学。Canvas 2D 本质上就是拿到CanvasRenderingContext2D然后通过它绘制形状、文字、图片等内容。(MDN Web Docs)一、Canvas 到底是什么最简单canvas idcanvas width500 height300/canvasJSconst canvas document.getElementById(canvas) const ctx canvas.getContext(2d) ctx.fillStyle red ctx.fillRect(100, 50, 200, 100)可以把 Canvas 理解成Canvas ↓ 一块“位图画布” ↓ JS 操作 ctx ↓ 往里面画东西和普通 DOM 最大区别DOM div spanhello/span /div 浏览器维护 元素 → 属性 → 样式 → 布局 → 渲染Canvascanvas ↓ 像素 ↓ 你自己画Canvas 画完以后浏览器并不知道你画了一个“按钮”还是“圆形”。所以Canvas 更像一个“画板”而不是 DOM 元素树。二、Canvas 基础1. 创建 Canvascanvas idcanvas/canvasconst canvas document.querySelector(#canvas) const ctx canvas.getContext(2d)常见 contextcanvas.getContext(2d) canvas.getContext(webgl)公司前端业务中2D Canvas 最常见。三、Canvas 宽高非常重要这是一个特别容易踩坑的地方。推荐canvas width800 height600/canvas而不是canvas { width: 800px; height: 600px; }两者不是一回事。Canvas 有两套尺寸Canvas 实际像素尺寸 ↓ width / height Canvas CSS 显示尺寸 ↓ style width / height例如canvas width800 height600 stylewidth:400px;height:300px /canvas实际绘制800 × 600显示400 × 300相当于浏览器进行了缩放。四、画矩形最常用的几个 API。fillRectctx.fillStyle red ctx.fillRect( 100, 50, 200, 100 )参数x y width heightstrokeRect只画边框ctx.strokeStyle blue ctx.lineWidth 5 ctx.strokeRect(100, 50, 200, 100)clearRect清除区域ctx.clearRect( 0, 0, canvas.width, canvas.height )动画里面非常常见。五、画路径Canvas 真正强大的地方是 Path。ctx.beginPath() ctx.moveTo(100, 100) ctx.lineTo(200, 100) ctx.lineTo(150, 200) ctx.closePath() ctx.stroke()画的是100,100 -------- 200,100 \ / \ / \ / 150,200六、beginPath 为什么重要例如ctx.beginPath() ctx.moveTo(100, 100) ctx.lineTo(200, 100) ctx.stroke()再画一条ctx.moveTo(100, 200) ctx.lineTo(200, 200) ctx.stroke()第一次stroke()绘制当前路径线 1路径还留在上下文中不清除moveTo/lineTo继续往同一个路径对象追加现在路径 线 1 线 2第二次stroke()把全部路径再次描边 →线 1 被重复绘制 2 次后果如果线条有透明度第一条线会变深如果换了strokeStyle颜色第一条线也会变成新颜色最经典坑推荐始终明确ctx.beginPath() ... ctx.stroke() ctx.beginPath() ... ctx.stroke()beginPath()的作用新建一条独立路径切断和之前旧路径的关联。stroke()/fill()只是把「当前所有累积的路径」画出来不会清空路径。否则路径可能不断累积。七、画线ctx.beginPath() ctx.moveTo(50, 50) ctx.lineTo(200, 100) ctx.lineTo(300, 50) ctx.stroke()常用ctx.lineWidth 2 ctx.strokeStyle #333 ctx.lineCap round ctx.lineJoin round属性控制什么常用值lineCap线段两端butt / round / squarelineJoin折线拐角miter / round / bevellineWidth线宽数字八、画圆ctx.beginPath() ctx.arc( 200, 150, 50, 0, Math.PI * 2 ) ctx.fill()参数x y radius startAngle //起始弧度 endAngle //结束弧度完整ctx.beginPath() //0度画到2Π ctx.arc( 200, 150, 50, 0, Math.PI * 2 ) ctx.fillStyle red ctx.fill()九、Canvas 颜色fillStyle填充颜色ctx.fillStyle #ff0000strokeStyle边框颜色ctx.strokeStyle #000透明度ctx.globalAlpha 0.5或者ctx.fillStyle rgba(255,0,0,0.5)十、渐变实际项目里也经常用。线性渐变//起点(0,0) → 终点(300,0) const gradient ctx.createLinearGradient( 0, 0, 300, 0 ) gradient.addColorStop(0, red) gradient.addColorStop(1, blue) // 把填充样式设置成这个渐变 ctx.fillStyle gradient // 画矩形使用上面的渐变填充在渐变范围内的形状都是渐变色的 ctx.fillRect(0, 0, 300, 100)径向渐变const gradient ctx.createRadialGradient( 150, 150, 20, 150, 150, 100 ) gradient.addColorStop(0, white) gradient.addColorStop(1, blue) ctx.fillStyle gradient ctx.fillRect(0, 0, 300, 300)十一、Canvas 文字ctx.font 30px Arial ctx.fillStyle black ctx.fillText( Hello Canvas, 100, 100 )strokeTextctx.strokeText( Hello, 100, 100 )文字对齐ctx.textAlign center ctx.textBaseline middle例如ctx.textAlign center // 水平对齐文字中心点对准 x坐标 ctx.textBaseline middle // 垂直对齐文字中心点对准 y坐标 ctx.fillText( Hello, canvas.width / 2, canvas.height / 2 )这个在验证码海报图片水印图表里面都非常常见。十二、保存和恢复状态这是 Canvas 很重要的 API。ctx.save() ctx.fillStyle red ctx.translate(100, 100) ctx.rotate(Math.PI / 4) ctx.fillRect(0, 0, 100, 100) ctx.restore()可以理解save() ↓ 保存当前绘图状态 修改各种属性 ↓ 绘制 restore() ↓ 恢复之前状态save()把当前画布所有绘图状态压栈保存restore()弹出恢复到上次 save 时的状态。⚠️ 重点保存的是【绘图状态】不是画布上已经画出来的像素已经画在画布的图形restore 不会擦掉。就能从之前的状态继续画实际开发非常推荐ctx.save() // 修改状态 // 绘制 ctx.restore()避免污染后面的绘制。十三、坐标变换Canvas 的高级开发经常会用到。translate移动坐标系ctx.translate(100, 50)之后ctx.fillRect(0, 0, 100, 100)实际上相当于从 100,50 开始画rotate旋转ctx.rotate(Math.PI / 4)注意Math.PI等于180°所以Math.PI / 2就是90°scalectx.scale(2, 2)相当于放大 2 倍。十四、图片绘制——公司最重要的一部分Canvas 很多实际项目不是“画图形”而是处理图片。核心 APIctx.drawImage()它支持多种图片源包括HTMLImageElement、HTMLVideoElement、HTMLCanvasElement、ImageBitmap等。(MDN Web Docs)最简单const img new Image() img.onload () { ctx.drawImage(img, 0, 0) } img.src /test.jpg注意一定要等img.onload否则图片可能还没加载完成。十五、图片缩放ctx.drawImage( img, 0, 0, 300, 200 )把图片画成300 × 200十六、图片裁剪drawImage最强大的地方之一。ctx.drawImage( img, sx, sy, sw, sh, dx, dy, dw, dh )前 4 个源裁切从原图挖一块sx源图片x 起点原图坐标系sy源图片y 起点sw源图片裁切宽度要挖多宽sh源图片裁切高度要挖多高后 4 个绘制到画布把挖出来的块画到 canvas5.dx画布上x 起点6.dy画布上y 起点7.dw画布上绘制的宽度拉伸 / 压缩8.dh画布上绘制的高度拉伸 / 压缩意思原图 ┌──────────────────┐ │ │ │ ┌──────┐ │ │ │裁剪区│ │ │ └──────┘ │ │ │ └──────────────────┘ ↓ Canvas ┌──────────────┐ │ │ │ 裁剪内容 │ │ │ └──────────────┘例如ctx.drawImage( img, 100, 100, 300, 300, 0, 0, 300, 300 )十七、公司实际场景 ①图片压缩这个非常重要。你之前关注过的用户上传原图 → 前端压缩 → WebP → OSSCanvas 就是核心技术之一。流程用户选择图片 ↓ File ↓ Image ↓ Canvas ↓ 限制最大宽高 ↓ drawImage ↓ toBlob ↓ 压缩后的 Blob/File ↓ OSS ↓ 拿到 URL一个实际的图片压缩函数function compressImage(file, maxWidth 2048, quality 0.8) { return new Promise((resolve, reject) { const img new Image() const url URL.createObjectURL(file) img.onload () { URL.revokeObjectURL(url) let { width, height } img let scale 1 // 调整图片缩放限制图片宽高像素太高的图片会被压像素会有个像素封顶值 if (width maxWidth) scale Math.min(scale, maxWidth / width) if (height maxHeight) scale Math.min(scale, maxHeight / height) width width * scale height height * scale const canvas document.createElement(canvas) canvas.width width canvas.height height const ctx canvas.getContext(2d) ctx.drawImage( img, 0, 0, width, height ) canvas.toBlob( blob { if (!blob) { reject(new Error(压缩失败)) return } resolve(blob) }, image/webp, quality ) } img.onerror reject img.src url }) }最大像素2048*2048 ≈ 400万像素内存固定上限≈16MB使用const blob await compressImage(file) const compressedFile new File( [blob], compressed.webp, { type: image/webp } )十八、toBlob vs toDataURL这是面试高频。toDataURLconst url canvas.toDataURL( image/jpeg, 0.8 )得到data:image/jpeg;base64,...缺点Canvas ↓ Base64 ↓ 字符串 ↓ 内存占用较大所以实际上传文件一般不推荐。toBlobcanvas.toBlob(blob { console.log(blob) }, image/jpeg, 0.8)得到Blob然后const file new File( [blob], image.jpg, { type: image/jpeg } )实际开发上传图片优先toBlob。MDN 也明确提供了toBlob()用于创建 Canvas 内容对应的 Blob。(GitHub)十九、公司实际场景 ②图片裁剪例如头像上传。用户选择图片 ↓ Canvas 显示图片 ↓ 用户拖动裁剪框 ↓ 计算裁剪区域 ↓ drawImage ↓ 生成 Blob ↓ 上传核心ctx.drawImage( img, cropX, cropY, cropWidth, cropHeight, 0, 0, outputWidth, outputHeight )这就是Canvas 图片裁剪器很多头像裁剪、证件照裁剪、商品图裁剪本质都是这个。二十、公司实际场景 ③水印例如用户上传图片 ↓ Canvas ↓ drawImage ↓ fillText(xxx公司) ↓ toBlob ↓ 上传代码ctx.drawImage( img, 0, 0, width, height ) ctx.font 30px Arial ctx.fillStyle rgba(255,255,255,0.5) ctx.fillText( Company, 30, height - 30 )二十一、公司实际场景 ④图片拼接例如把图片 A 图片 B 图片 C拼成┌─────────┐ │ 图片 A │ ├─────────┤ │ 图片 B │ ├─────────┤ │ 图片 C │ └─────────┘Canvasctx.drawImage(img1, 0, 0) ctx.drawImage(img2, 0, height1) ctx.drawImage(img3, 0, height1 height2)最后canvas.toBlob(...)可以生成一张新图片。二十二、公司实际场景 ⑤截图这个很常见但要注意Canvas 本身不能直接把任意 DOM “截图”。通常是DOM ↓ html2canvas 等库 ↓ Canvas ↓ Blob / Image或者浏览器 Screenshot API所以面试不要说Canvas 可以直接截网页。更准确Canvas 可以作为截图结果的绘制载体DOM 截图通常需要额外的 DOM → Canvas 方案。二十三、公司实际场景 ⑥验证码例如┌─────────────┐ │ A 7 K 2 │ │ \ /\/\/ │ │ 噪声 │ └─────────────┘Canvasctx.font 30px Arial ctx.fillText(A7K2, 20, 40)然后ctx.beginPath() ctx.moveTo(...) ctx.lineTo(...) ctx.stroke()加入随机颜色随机字体随机旋转干扰线噪点二十四、公司实际场景 ⑦签名板这个非常典型。比如用户在手机上签名 ┌───────────────────────┐ │ │ │ 手写签名 │ │ │ └───────────────────────┘核心canvas.addEventListener( pointerdown, e { ctx.beginPath() ctx.moveTo(e.offsetX, e.offsetY) } ) canvas.addEventListener( pointermove, e { ctx.lineTo(e.offsetX, e.offsetY) ctx.stroke() } )实际开发建议使用PointerEvent而不是分别处理mousedown mousemove touchstart touchmove因为 Pointer Events 可以统一鼠标、触摸、笔。二十五、公司实际场景 ⑧画板类似Photoshop 白板 在线涂鸦 流程图 电子签名基本结构Canvas ↓ PointerDown ↓ 开始路径 PointerMove ↓ 不断 lineTo PointerUp ↓ 结束例如let drawing false canvas.addEventListener(pointerdown, e { drawing true ctx.beginPath() ctx.moveTo(e.offsetX, e.offsetY) }) canvas.addEventListener(pointermove, e { if (!drawing) return ctx.lineTo(e.offsetX, e.offsetY) ctx.stroke() }) canvas.addEventListener(pointerup, () { drawing false })二十六、公司实际场景 ⑨图表例如销量 │ │ ● │ ● │ ● │ ● └────────────────可以自己使用 Canvas 实现折线图柱状图饼图雷达图热力图不过公司实际开发通常业务图表 ↓ ECharts / AntV 等 ↓ 底层可能使用 Canvas/SVG所以你不一定需要手写完整图表库。但是理解 Canvas 有助于理解为什么 ECharts Canvas 模式能够渲染大量数据。二十七、公司实际场景 ⑩动画Canvas 非常适合动画。核心requestAnimationFrame()例如小球let x 0 function render() { ctx.clearRect( 0, 0, canvas.width, canvas.height ) ctx.beginPath() ctx.arc( x, 100, 20, 0, Math.PI * 2 ) ctx.fill() x 2 requestAnimationFrame(render) } render()动画流程requestAnimationFrame ↓ 清空 Canvas ↓ 更新状态 ↓ 重新绘制 ↓ requestAnimationFrame ↓ ...二十八、为什么动画不用 setInterval不推荐setInterval(() { render() }, 16)推荐requestAnimationFrame(render)因为它更适合浏览器渲染节奏。基本思想JS 更新 ↓ 浏览器下一帧 ↓ 绘制二十九、Canvas 动画性能这是公司面试非常喜欢问的。假设10000 个粒子每一帧都clearRect() for (...) { draw() }主线程压力可能很大。优化方向1. 减少绘制数量10000 → 30002. 减少状态切换不要fillStyle red draw() fillStyle blue draw() fillStyle red draw()尽量fillStyle red for (...) { draw() }三十、批量绘制例如ctx.beginPath() for (const point of points) { ctx.moveTo(point.x, point.y) ctx.arc( point.x, point.y, 2, 0, Math.PI * 2 ) } ctx.fill()相比大量beginPath() fill() beginPath() fill()通常更合理。三十一、Canvas 高 DPI / 高清屏这是实际开发非常重要的一块。例如普通屏幕 devicePixelRatio 1 Retina devicePixelRatio 2如果直接canvas width500 height300可能出现文字模糊 线条模糊 图片模糊常见解决方案const dpr window.devicePixelRatio || 1 const rect canvas.getBoundingClientRect() canvas.width rect.width * dpr canvas.height rect.height * dpr ctx.scale(dpr, dpr)这样CSS 500 × 300 实际 Canvas 1000 × 600Retina 屏幕就能得到更好的清晰度。三十二、但是这里有一个坑如果你canvas.width 1000 canvas.height 600然后canvas { width: 500px; height: 300px; }那么你必须明确逻辑坐标 500 × 300 实际像素 1000 × 600所以项目里通常要把CSS 尺寸 逻辑尺寸 Canvas bitmap 尺寸区分开。三十三、Canvas 和 DOM 事件Canvas 本身是一个元素canvas/canvas所以可以监听canvas.addEventListener(click, ...)但是 Canvas 内部你画的矩形 你画的圆 你画的文字都不是 DOM。所以document.querySelector(.circle)找不到你画的圆。三十四、Canvas 如何实现点击某个图形需要自己做鼠标坐标 ↓ 判断 ↓ 是否落在图形区域例如圆const dx mouseX - circle.x const dy mouseY - circle.y const distance Math.sqrt( dx * dx dy * dy ) if (distance circle.radius) { console.log(点击了圆) }这叫Hit Testing / 命中检测Canvas 编辑器、白板、图形编辑器都会大量使用。三十五、矩形命中检测function isPointInRect( x, y, rect ) { return ( x rect.x x rect.x rect.width y rect.y y rect.y rect.height ) }三十六、一个非常实用的技巧isPointInPathCanvas 自己提供了路径命中检测const path new Path2D() path.rect(100, 100, 200, 100) ctx.fill(path) canvas.addEventListener(click, e { const rect canvas.getBoundingClientRect() const x e.clientX - rect.left const y e.clientY - rect.top if (ctx.isPointInPath(path, x, y)) { console.log(点击到了) } })这个在 Canvas 图形编辑器里很好用。三十七、Canvas 图片跨域问题这是面试必问坑点之一。假设const img new Image() img.src https://xxx.com/a.jpg img.onload () { ctx.drawImage(img, 0, 0) canvas.toBlob(...) }可能报SecurityError原因跨域图片没有正确 CORS 授权Canvas 被污染tainted。一旦 Canvas 被污染canvas.toBlob() canvas.toDataURL() ctx.getImageData()都可能抛出SecurityError。(MDN Web Docs)三十八、怎么解决前端const img new Image() img.crossOrigin anonymous img.src url但是光前端这样写不够。服务端还需要Access-Control-Allow-Origin: https://your-site.com或者合适的允许来源配置。所以Canvas 跨域图片问题是“前端 服务端 CORS”共同解决的。MDN 也明确说明了crossorigin属性需要和服务端 CORS 响应头配合。(MDN Web Docs)三十九、Canvas 清空的几个方式clearRectctx.clearRect( 0, 0, canvas.width, canvas.height )最常用。重置 widthcanvas.width canvas.width也能清空。但是会重置 Canvas 的绘图状态。所以一般不推荐作为普通清屏手段。四十、Canvas 撤销 / 重做怎么实现Canvas 不自带undo redo公司项目一般自己做。方案一保存图片const history [] history.push( canvas.toDataURL() )撤销history.pop() ↓ 重新 drawImage但是数据量大时非常占内存。更好的方式保存操作[ { type: line, points: [...] }, { type: circle, x: 100, y: 100 } ]然后state ↓ 重新 render ↓ Canvas这其实就是Canvas 状态模型更适合复杂编辑器。四十一、Canvas 和 Web Worker如果 Canvas 任务很重大量粒子 复杂图片处理 大图处理 复杂绘制可以考虑主线程 ↓ Worker ↓ OffscreenCanvasOffscreenCanvas可以让 Canvas 渲染脱离 DOM并支持在 Worker 中进行渲染从而把一部分重任务从主线程移走。(MDN Web Docs)基本const offscreen canvas.transferControlToOffscreen() const worker new Worker(./worker.js) worker.postMessage( { canvas: offscreen }, [offscreen] )Workerself.onmessage e { const canvas e.data.canvas const ctx canvas.getContext(2d) // 绘制 }这个属于Canvas 性能进阶。不是普通业务一开始就需要。四十二、Canvas 性能优化总结公司实际开发可以记这一套Canvas 性能 │ ├── 减少绘制次数 │ ├── 减少状态切换 │ ├── 批量绘制 │ ├── requestAnimationFrame │ ├── 避免频繁创建对象 │ ├── 控制 Canvas 分辨率 │ ├── 大量计算放 Worker │ ├── OffscreenCanvas │ └── 必要时使用 WebGL四十三、Canvas 和 SVG 怎么选这个也是面试高频。CanvasSVG本质位图矢量 DOM元素数量多时更适合太多可能压力大单个元素操作不方便很方便事件自己做命中检测DOM 事件放大可能失真不失真图片处理很强一般游戏很适合一般图片编辑很适合一般图标一般很适合流程图可以很适合数据可视化很适合也很适合简单记大量像素 大量绘制 动画 图片处理 游戏 ↓ Canvas大量独立图形 需要点击单个元素 需要 DOM 结构 需要无损缩放 ↓ SVG四十四、Canvas 和 WebGL 怎么选进一步Canvas 2D ↓ CPU / 浏览器 2D 绘制模型 WebGL ↓ GPU ↓ 大量图形 / 3D / 高性能例如普通图片处理 签名 海报 验证码 简单动画 → Canvas 2D3D 大量粒子 复杂游戏 地图 GPU 大规模计算 → WebGL / WebGPU四十五、公司项目最常见的 Canvas 场景你可以直接记这张表场景Canvas 使用图片压缩⭐⭐⭐⭐⭐图片裁剪⭐⭐⭐⭐⭐图片格式转换⭐⭐⭐⭐⭐图片水印⭐⭐⭐⭐⭐图片拼接⭐⭐⭐⭐签名板⭐⭐⭐⭐⭐画板⭐⭐⭐⭐⭐验证码⭐⭐⭐⭐海报生成⭐⭐⭐⭐⭐截图⭐⭐⭐⭐Canvas 图表⭐⭐⭐⭐粒子动画⭐⭐⭐⭐⭐游戏⭐⭐⭐⭐⭐视频帧处理⭐⭐⭐⭐PDF/文档预览中的图形处理⭐⭐⭐OCR 前图片预处理⭐⭐⭐⭐大数据可视化⭐⭐⭐⭐四十六、如果你是做 Vue / React实际项目不要把 Canvas 所有逻辑直接塞组件里。不推荐function App() { // 500 行 Canvas 代码 }建议components/ CanvasEditor.jsx hooks/ useCanvas.js utils/ canvas/ image.js draw.js coordinate.js hitTest.js export.js例如function useCanvas() { const canvasRef useRef(null) function draw() { const canvas canvasRef.current const ctx canvas.getContext(2d) // ... } return { canvasRef, draw } }Vueconst canvasRef ref(null) onMounted(() { const canvas canvasRef.value const ctx canvas.getContext(2d) })四十七、公司实际开发推荐架构如果做一个 Canvas 编辑器React/Vue │ ↓ CanvasEditor │ ┌─────────┴─────────┐ ↓ ↓ State Canvas │ │ ↓ ↓ shapes / images render() │ │ └─────────┬─────────┘ ↓ Interaction │ ┌───────────┼───────────┐ ↓ ↓ ↓ Pointer HitTest Keyboard核心思想不要把 Canvas 当成状态本身。而应该业务状态 ↓ render ↓ Canvas例如const shapes [ { type: rect, x: 100, y: 100, width: 200, height: 100 }, { type: circle, x: 400, y: 200, radius: 50 } ]然后function render(ctx, shapes) { ctx.clearRect( 0, 0, canvas.width, canvas.height ) for (const shape of shapes) { drawShape(ctx, shape) } }这个思路非常重要。四十八、一个真正比较像公司的 Canvas 项目建议你练「图片编辑器」功能┌─────────────────────────────┐ │ 图片编辑器 │ ├─────────────────────────────┤ │ │ │ Canvas │ │ │ │ ┌──────────┐ │ │ │ 图片 │ │ │ └──────────┘ │ │ │ ├─────────────────────────────┤ │ 上传 │ 裁剪 │ 旋转 │ 水印 │ │ 压缩 │ 撤销 │ 重做 │ 导出 │ └─────────────────────────────┘你会一次性学会Canvas 基础 ↓ drawImage ↓ 图片缩放 ↓ 图片裁剪 ↓ 旋转 ↓ 坐标转换 ↓ 鼠标/触摸事件 ↓ 命中检测 ↓ 撤销/重做 ↓ toBlob ↓ 文件上传 ↓ 高 DPI ↓ 性能优化这比单纯练画圆 画矩形 画线有价值得多。四十九、Canvas 面试重点如果你准备前端面试我建议重点掌握这15 个问题基础Canvas 和 SVG 区别getContext(2d)是什么Canvas width/height 和 CSS width/height 有什么区别fillRect、strokeRect、clearRectbeginPath()有什么作用save()/restore()translate/rotate/scale图片drawImage()三种参数形式如何用 Canvas 压缩图片toBlob()和toDataURL()区别Canvas 图片跨域为什么会污染Canvas 如何做图片裁剪性能Canvas 如何实现动画为什么用requestAnimationFrameCanvas 卡顿怎么优化再往上Canvas ↓ OffscreenCanvas ↓ Web Worker ↓ WebGL ↓ WebGPU属于进阶路线。五十、你可以按这个顺序学如果你的目标是公司实际开发 面试我建议不要平均用力第一阶段基础 │ ├── Canvas 创建 ├── 坐标系 ├── 矩形 ├── Path ├── 圆 ├── 文字 ├── 渐变 └── save / restore ↓ 第二阶段图片 │ ├── Image ├── drawImage ├── 图片缩放 ├── 图片裁剪 ├── toBlob ├── toDataURL ├── 图片压缩 ├── WebP └── 水印 ↓ 第三阶段交互 │ ├── PointerEvent ├── 坐标转换 ├── 拖动 ├── 缩放 ├── 旋转 ├── HitTest └── isPointInPath ↓ 第四阶段动画 │ ├── requestAnimationFrame ├── 动画状态 ├── 粒子 └── 性能优化 ↓ 第五阶段工程化 │ ├── React/Vue ├── 高 DPI ├── 撤销/重做 ├── 状态管理 ├── Worker └── OffscreenCanvas其中对普通前端岗位来说最值得你重点掌握的是drawImage 图片压缩/裁剪 toBlob PointerEvent 坐标转换 高 DPI RAF 跨域 Canvas 性能。尤其是你之前做的前端图片预处理 → 压缩 → WebP → OSS → AI这条链路Canvas 正好是其中非常核心的一环。实际生产里要特别注意超大图片导致的内存/主线程阻塞、CORS、EXIF 方向、输出质量和文件大小这些问题其中跨域图片如果没有 CORS 授权会直接影响toBlob()、toDataURL()等读取操作。(MDN Web Docs)如果按“真正能拿去面试”的方式继续我建议下一步直接做一套《Canvas 10 个公司实战案例》图片压缩 → 裁剪器 → 图片旋转 → 水印 → 签名板 → 橡皮擦 → 撤销/重做 → 海报生成器 → Canvas 粒子动画 → React/Vue Canvas 编辑器每个案例都带完整代码和面试考点。