Canvas涂鸦核心是状态管理、坐标校准与路径隔离;mousedown须调beginPath()和moveTo(),用getBoundingClientRect()算真实坐标,mousemove只lineTo()不重复beginPath(),lineCap/lineJoin设为"round",清空用clearRect()而非重置width。

Canvas涂鸦板不是“画几条线”就完事,核心是状态管理 + 坐标校准 + 路径隔离。漏掉任何一环,都会出现断线、飞线、卡顿或高DPR屏下糊成一片。
mousedown里必须调用beginPath()和moveTo()
这是最常被跳过的初始化动作。不调用beginPath(),后续所有lineTo()都追加到上一条路径;不调用moveTo(),lineTo()会从(0, 0)连过来——这就是为什么松开鼠标再点一下,线总从左上角“炸”出来。
-
mousedown中:先ctx.beginPath(),再ctx.moveTo(x, y),同时设isDrawing = true - 别用
e.offsetX/e.offsetY:它们受CSS缩放、外边距、transform干扰,结果偏移不可控 - 务必用
canvas.getBoundingClientRect()算真实坐标:e.clientX - rect.left
mousemove中只lineTo()不beginPath()
每次移动都新建路径,等于把整段轨迹拆成无数个独立短线,既断续又慢。正确做法是保持一个开放路径,持续追加端点。
- 仅当
isDrawing === true时执行ctx.lineTo(x, y)和ctx.stroke() - 千万别在
mousemove里重复写ctx.beginPath()——这会让每帧都重绘全部历史路径,画两笔就卡 - 如果想支持“抬起再点继续画”,
mouseup和mouseout都要设isDrawing = false
lineCap和lineJoin必须设为"round"
默认"butt"端点像刀切,相邻线段之间露白缝;"miter"拐角尖锐突兀。涂鸦需要的是“手绘感”,不是工程制图。
-
ctx.lineCap = "round"让每一段线头带圆弧,拼接顺滑 -
ctx.lineJoin = "round"让折线拐角也变圆,避免生硬棱角 - 线宽建议从
4起步试,1太细易断,12以上在快速拖动时容易糊成带状
清空画布别用canvas.width = canvas.width
这行代码看似重置,实则重置整个canvas缓冲区,会丢弃CSS缩放、破坏高DPR适配,还可能触发强制重排。更糟的是:若你用CSS设置了width: 100%,它会让内容被拉伸变形。
- 清空唯一正解:
ctx.clearRect(0, 0, canvas.width, canvas.height) - 如需保留缩放适配(比如Retina屏),记得提前缓存
dpr,并在clearRect()参数中乘上它 - 移动端要额外监听
touchstart/touchmove,且取touches[0]而非changedTouches[0],坐标同样过getBoundingClientRect()
真正难的不是画出第一笔,而是让连续移动的每一帧都落在预期位置——坐标不准、路径没隔离、端点没圆化,三个问题叠加,涂鸦就变成“猜线游戏”。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











