You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Canvas中实现Ctrl+Z撤销功能?

Canvas实现Ctrl+Z撤销操作的解决方案

我正尝试为Canvas实现类似Ctrl + Z的撤销操作,但完全不知道该怎么做。尝试过使用split(-1,1)但无效,要么直接全部删除,无法实现撤销效果。以下是现有代码,想添加Ctrl+Z事件却无从下手:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const modeBtn = document.querySelector("#mode-btn");

const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 800;

canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
ctx.lineCap = "round";

let isPainting = false;
let isFilling = false;

function onMove(event) {
  if (isPainting) {
    ctx.lineTo(event.offsetX, event.offsetY);
    ctx.stroke();
    return;
  }
  ctx.moveTo(event.offsetX, event.offsetY);
}

function startPainting() {
  isPainting = true;
}

function cancelPainting() {
  isPainting = false;
  ctx.beginPath();
}

function onModeClick() {
  if (isFilling) {
    isFilling = false;
  } else {
    isFilling = true;
  }
}

function onCanvasClick() {
  if (isFilling) {
    ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  }
}

canvas.addEventListener("mousemove", onMove);
canvas.addEventListener("mousedown", startPainting);
canvas.addEventListener("mouseup", cancelPainting);
canvas.addEventListener("mouseleave", cancelPainting);
canvas.addEventListener("click", onCanvasClick);

modeBtn.addEventListener("click", onModeClick);
<canvas></canvas>
    <div class="btns">
      <button id="mode-btn">Draw</button>
    </div>
    <script src="app.js"></script>

要实现Canvas的撤销功能,核心思路是维护一个画布状态的历史快照数组——每次完成一笔绘制或填充操作后,把当前画布的像素数据保存到数组里;触发撤销时,恢复上一个快照的状态。具体修改如下:

关键修改步骤

  • 新增快照数组和当前索引变量,用于管理历史状态
  • 在初始化时保存空白画布的初始状态
  • 每次绘制结束(鼠标抬起)或填充画布后,将当前状态存入快照数组
  • 添加键盘事件监听,捕获Ctrl+Z组合键触发撤销
  • 实现撤销函数,恢复上一个有效快照

修改后的完整代码

JavaScript部分

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const modeBtn = document.querySelector("#mode-btn");

const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 800;

canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
ctx.lineCap = "round";

let isPainting = false;
let isFilling = false;
// 新增:历史快照数组 + 当前快照索引
let historySnapshots = [];
let currentSnapshotIndex = -1;

// 保存当前画布状态到快照数组
function saveSnapshot() {
  // 获取当前画布的像素数据
  const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  // 如果当前不是最后一个快照,截断后面的(避免撤销后再绘制覆盖历史)
  if (currentSnapshotIndex < historySnapshots.length - 1) {
    historySnapshots = historySnapshots.slice(0, currentSnapshotIndex + 1);
  }
  historySnapshots.push(imageData);
  currentSnapshotIndex++;
}

// 撤销操作:恢复上一个快照
function undo() {
  if (currentSnapshotIndex > 0) {
    currentSnapshotIndex--;
    const prevSnapshot = historySnapshots[currentSnapshotIndex];
    ctx.putImageData(prevSnapshot, 0, 0);
  }
}

function onMove(event) {
  if (isPainting) {
    ctx.lineTo(event.offsetX, event.offsetY);
    ctx.stroke();
    return;
  }
  ctx.moveTo(event.offsetX, event.offsetY);
}

function startPainting() {
  isPainting = true;
}

function cancelPainting() {
  isPainting = false;
  ctx.beginPath();
  // 绘制结束后保存快照
  saveSnapshot();
}

function onModeClick() {
  if (isFilling) {
    isFilling = false;
    modeBtn.textContent = "Draw";
  } else {
    isFilling = true;
    modeBtn.textContent = "Fill";
  }
}

function onCanvasClick() {
  if (isFilling) {
    ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    // 填充后保存快照
    saveSnapshot();
  }
}

// 监听键盘事件:Ctrl+Z触发撤销
function onKeyDown(event) {
  if (event.ctrlKey && event.key === "z") {
    event.preventDefault(); // 阻止浏览器默认撤销行为
    undo();
  }
}

// 初始化:保存空白画布的初始状态
saveSnapshot();

canvas.addEventListener("mousemove", onMove);
canvas.addEventListener("mousedown", startPainting);
canvas.addEventListener("mouseup", cancelPainting);
canvas.addEventListener("mouseleave", cancelPainting);
canvas.addEventListener("click", onCanvasClick);
modeBtn.addEventListener("click", onModeClick);
document.addEventListener("keydown", onKeyDown);

HTML部分

<canvas></canvas>
<div class="btns">
  <button id="mode-btn">Draw</button>
</div>
<script src="app.js"></script>

关键代码解释

  1. 快照管理:
    • saveSnapshot()使用ctx.getImageData()获取画布完整像素数据,存入数组;如果撤销后再次绘制,会截断后续的快照,避免历史混乱。
    • undo()检查当前索引是否大于0(确保有可撤销的状态),然后恢复上一个快照的像素数据。
  2. 触发时机:
    • 绘制结束(mouseup/mouseleave)和填充画布后调用saveSnapshot(),确保每一步操作都被记录。
  3. 键盘事件:
    • 监听keydown事件,判断ctrlKey和key === 'z',触发撤销时阻止浏览器默认行为,避免冲突。

内容的提问来源于stack exchange,提问作者oz527

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 14:25:23