一、功能介绍
本文实现了一个基于 Canvas 的简易绘图应用,支持以下功能:
- ✏️ 鼠标绘制自由路径
- 🔄 撤销操作
- 🔁 重做操作
- 💾 保存绘制结果为图片
二、技术实现
1. 核心数据结构
使用指针和数组组合的方式管理绘制历史:
pointer:指针变量,记录当前绘制状态的索引contentArr:数组,存储所有绘制路径的完整历史
这种方式的优势在于:
- 无需维护两个数组(撤销栈和重做栈)
- 操作逻辑更简洁
- 内存使用更高效
2. 核心代码分析
初始化变量
// 定义一个指针
var pointer = -1;
// 定义一个数组
var contentArr = [];
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
let isDrawing = false;
let currentPath = null;
绘制逻辑
// 鼠标按下事件
canvas.addEventListener("mousedown", e => {
isDrawing = true;
currentPath = {
color: context.strokeStyle,
width: context.lineWidth,
points: [{ x: e.offsetX, y: e.offsetY }],
};
pointer++;
});
// 鼠标移动事件
canvas.addEventListener("mousemove", e => {
if (!isDrawing) return;
currentPath.points.push({ x: e.offsetX, y: e.offsetY });
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths([...contentArr, currentPath]);
});
// 鼠标松开事件
canvas.addEventListener("mouseup", e => {
isDrawing = false;
contentArr.push(currentPath);
});
撤销/重做逻辑
// 撤销按钮点击事件
undoBtn.addEventListener("click", e => {
// 如果当前指针在第一个元素上,不执行撤销操作
if (pointer < 0) return;
pointer--;
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths(contentArr);
});
// 重做按钮点击事件
redoBtn.addEventListener("click", e => {
// 如果当前指针在最后一个元素,不执行恢复操作
if (pointer == contentArr.length - 1) return;
pointer++;
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths(contentArr);
});
绘制函数
// 画所有的路径
function drawPaths(paths) {
paths.forEach((path, index) => {
if (index <= pointer) {
context.beginPath();
context.strokeStyle = path.color;
context.lineWidth = path.width;
context.moveTo(path.points[0].x, path.points[0].y);
path.points.slice(1).forEach(point => {
context.lineTo(point.x, point.y);
});
context.stroke();
}
});
}
三、技术要点
指针管理:通过
pointer变量控制当前显示的绘制状态,撤销时指针左移,重做时指针右移路径存储:每条路径包含颜色、线宽和点坐标数组,完整记录绘制信息
实时预览:鼠标移动时实时更新画布,提供流畅的绘制体验
状态判断:撤销/重做操作前检查指针位置,避免越界
绘制优化:只绘制指针范围内的路径,提高绘制效率
四、应用场景
- 在线绘图工具
- 电子签名功能
- 简单的图形编辑器
- 教育类应用中的绘图功能
五、代码优化建议
性能优化:对于复杂绘制,可以考虑使用离屏 Canvas 或 WebGL 提高性能
功能扩展:
- 添加颜色选择器
- 添加线条粗细调节
- 添加橡皮擦功能
- 支持多种图形绘制(直线、矩形、圆形等)
用户体验:
- 添加绘制完成后的视觉反馈
- 实现快捷键支持(Ctrl+Z 撤销,Ctrl+Y 重做)
- 优化移动端触摸支持
六、总结
本文介绍的指针式撤销重做实现方案,通过一个指针变量和一个数组,简洁高效地实现了 Canvas 绘图的撤销重做功能。这种实现方式逻辑清晰,代码简洁,适合中小型绘图应用使用。
通过理解这种实现思路,你可以将其应用到更多需要历史记录和状态管理的场景中,如文本编辑器、表单填写等。
相关链接
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>指针写法</title>
</head>
<style>
#canvas {
border: 1px solid #ccc;
cursor: crosshair;
margin-bottom: 10px;
}
button {
margin: 5px;
padding: 5px 10px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
button:hover {
background-color: #f0f0f0;
}
#resultImg {
margin-top: 10px;
max-width: 400px;
border: 1px solid #ccc;
}
</style>
<body>
<div>
<canvas id="canvas" width="400" height="400"></canvas>
<br />
<button id="undoBtn">撤销</button>
<button id="redoBtn">重做</button>
<button id="saveBtn">保存</button>
<br />
<img id="resultImg" />
</div>
<script>
// 定义一个指针
var pointer = -1;
// 定义一个数组
var contentArr = [];
const canvas = document.getElementById("canvas");
const undoBtn = document.getElementById("undoBtn");
const redoBtn = document.getElementById("redoBtn");
const saveBtn = document.getElementById("saveBtn");
const context = canvas.getContext("2d");
const resultImg = document.getElementById("resultImg");
// 设置默认样式
context.strokeStyle = "#000000";
context.lineWidth = 2;
context.lineCap = "round";
context.lineJoin = "round";
let isDrawing = false;
let currentPath = null;
// 鼠标按下事件
canvas.addEventListener("mousedown", e => {
isDrawing = true;
currentPath = {
color: context.strokeStyle,
width: context.lineWidth,
points: [{ x: e.offsetX, y: e.offsetY }],
};
pointer++;
});
// 鼠标移动事件
canvas.addEventListener("mousemove", e => {
if (!isDrawing) return;
currentPath.points.push({ x: e.offsetX, y: e.offsetY });
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths([...contentArr, currentPath]);
});
// 鼠标松开事件
canvas.addEventListener("mouseup", e => {
isDrawing = false;
contentArr.push(currentPath);
});
// 鼠标离开事件
canvas.addEventListener("mouseleave", e => {
isDrawing = false;
});
// 撤销按钮点击事件
undoBtn.addEventListener("click", e => {
// 如果当前指针在第一个元素上,不执行撤销操作
if (pointer < 0) return;
pointer--;
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths(contentArr);
});
// 重做按钮点击事件
redoBtn.addEventListener("click", e => {
// 如果当前指针在最后一个元素,不执行恢复操作
if (pointer == contentArr.length - 1) return;
pointer++;
context.clearRect(0, 0, canvas.width, canvas.height);
drawPaths(contentArr);
});
// 保存按钮点击事件
saveBtn.addEventListener("click", e => {
const dataUrl = canvas.toDataURL();
resultImg.src = dataUrl;
});
// 画所有的路径
function drawPaths(paths) {
paths.forEach((path, index) => {
if (index <= pointer) {
context.beginPath();
context.strokeStyle = path.color;
context.lineWidth = path.width;
context.moveTo(path.points[0].x, path.points[0].y);
path.points.slice(1).forEach(point => {
context.lineTo(point.x, point.y);
});
context.stroke();
}
});
}
</script>
</body>
</html>