在网页中实现一个简单的涂鸦板,核心是让画布接收鼠标或触摸轨迹,并把这些轨迹实时渲染到Canvas上。使用jQuery可以快速完成事件监听和DOM操作,而导出为PNG图片则依赖Canvas自带的toDataURL方法。整个过程不需要插件,也不需要服务端参与,适合在线签名、图片标注、白板工具等轻量场景。

一、页面结构与基础配置
涂鸦板的HTML结构不复杂,一个<canvas>元素加上几个控制按钮即可。比较容易被忽略的是<canvas>的width和height属性与CSS宽度高度之间的关系。前者决定画布的实际像素坐标,后者只影响页面显示尺寸。如果两者不一致,绘图坐标与鼠标位置之间就会出现偏差。
先给canvas设置固定绘图区域,设置为800×500像素,同时在CSS里设置边框和白色背景。白色背景可以让导出的PNG图片不透明,并且在视觉上更接近纸张。页面引入jQuery后,jQuery对象通过$('#doodle')[0]转换为原生DOM对象,这样才能调用getContext方法。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>jQuery Canvas 涂鸦板</title>
<style>
#doodle {
border: 1px solid #ccc;
cursor: crosshair;
background: #fff;
}
.toolbar {
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="toolbar">
<button id="clearBtn">清空</button>
<button id="exportBtn">导出PNG</button>
<span id="status"></span>
</div>
<canvas id="doodle" width="800" height="500"></canvas>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
// 绘图逻辑将在下一个小节实现
</script>
</body>
</html>
这里把导出和清空按钮放在画布上方,状态区域用于提示导出结果。后面所有事件都基于这个结构来扩展。
二、用jQuery绑定鼠标与触摸绘图事件
绘图的核心是三个鼠标事件mousedown、mousemove和mouseup。在mousedown阶段记录起始点并开启绘制标志,在mousemove阶段不断连接新的坐标点并调用stroke绘制,在mouseup阶段结束当前路径。移动端还需要处理touchstart、touchmove和touchend。
坐标换算要格外注意。鼠标事件中的clientX和clientY是相对浏览器视口的位置,而Canvas绘图使用自己的像素坐标。通过getBoundingClientRect获取canvas在页面中的位置,再用client坐标减去左上角偏移,最后乘以实际像素与显示尺寸的比例,就能得到准确的绘制坐标。这样做可以同时兼容CSS缩放和移动端页面缩放。
$(function () {
var canvas = $('#doodle')[0];
var ctx = canvas.getContext('2d');
var drawing = false;
var lastX = 0;
var lastY = 0;
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#333';
function getPos(e) {
var rect = canvas.getBoundingClientRect();
var clientX, clientY;
if (e.originalEvent.touches) {
var touch = e.originalEvent.touches[0];
clientX = touch.clientX;
clientY = touch.clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
return {
x: (clientX - rect.left) * (canvas.width / rect.width),
y: (clientY - rect.top) * (canvas.height / rect.height)
};
}
$('#doodle').on('mousedown touchstart', function (e) {
e.preventDefault();
drawing = true;
var pos = getPos(e);
lastX = pos.x;
lastY = pos.y;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
});
$(document).on('mousemove touchmove', function (e) {
if (!drawing) return;
e.preventDefault();
var pos = getPos(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
lastX = pos.x;
lastY = pos.y;
});
$(document).on('mouseup touchend', function () {
drawing = false;
ctx.closePath();
});
});
这里将mousemove和touchmove绑定在document上而不是canvas上,是为了避免鼠标快速移动时移出画布导致线条中断。由于移动端触摸事件不会触发mousemove,所以需要同时绑定touch事件。调用preventDefault可以阻止触摸滚动页面。
三、导出为PNG图片
涂鸦完成后,导出PNG的核心方法只有一行:canvas.toDataURL('image/png')。它会返回一个base64格式的字符串,可以直接赋值给下载链接的href属性。为了让浏览器主动下载而不是跳转,需要创建一个不可见的下载链接,设置download属性,然后触发click事件。
$('#exportBtn').on('click', function () {
var url = canvas.toDataURL('image/png');
var link = document.createElement('a');
link.href = url;
link.download = 'doodle.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
$('#status').text('已导出PNG图片');
});
使用toDataURL导出的图片默认会保留Canvas的透明区域。如果画布没有先填充白色,PNG背景可能是透明的。部分图片查看器会用黑色或网格显示透明区域,所以通常需要在初始化时执行ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height);,或者在导出前临时填充背景。
toDataURL会生成体积较大的base64数据,如果画布面积很大,建议改用toBlob方法。toBlob直接生成二进制Blob对象,再通过URL.createObjectURL生成临时下载地址,下载完成后调用URL.revokeObjectURL释放内存。两种方式都能得到PNG文件,toBlob在处理高清画布时更节约内存。
四、常见问题与优化建议
高分屏设备上直接使用CSS像素绘制会导致图形模糊。原因是Canvas内部像素与实际屏幕物理像素不匹配。解决思路是先读取window.devicePixelRatio,把canvas的width和height属性设置为显示尺寸乘以该倍率,再用CSS控制显示尺寸,最后在绘图上下文中调用ctx.scale(ratio, ratio)。这样线条会按照更高分辨率渲染,导出图片也会更清晰。
频繁触发mousemove时,每次移动都执行stroke可能会造成性能抖动,尤其在移动设备上。如果只是简单涂鸦,当前方案基本够用;如果需要加入更复杂的笔刷或大尺寸画布,可以将坐标点先存入数组,再通过requestAnimationFrame集中绘制。这样做可以避免同一次事件循环内重复渲染。
清空功能可以通过ctx.clearRect再重新填充背景实现。撤销功能则需要额外的数据模型,常见做法是保存每次绘制完成后的图片数据,回退时重新绘制上一步的图像。涂鸦板整体逻辑并不复杂,掌握Canvas路径绘制和事件坐标换算后,可以根据业务需求灵活扩展颜色选择、笔刷粗细、橡皮擦等功能。

