add undo/redo support

This commit is contained in:
Shuanglei Tao
2025-10-26 12:41:22 +08:00
parent a560427a92
commit bf6df54f49
5 changed files with 111 additions and 0 deletions

View File

@@ -419,6 +419,10 @@ canvas.text-placement-mode {
border-color: var(--primary-color);
}
.tool-button.hide {
display: none;
}
body.debug-mode .tool-button {
background-color: var(--dark-input-bg);
border-color: var(--dark-border);

View File

@@ -146,7 +146,11 @@
<button id="brush-mode" title="画笔" class="tool-button">✏️</button>
<button id="eraser-mode" title="橡皮擦" class="tool-button">🧽</button>
<button id="text-mode" title="添加文字" class="tool-button">T</button>
<button id="undo-btn" title="撤销 (Ctrl+Z)" class="tool-button hide"></button>
<button id="redo-btn" title="重做 (Ctrl+Y)" class="tool-button hide"></button>
</div>
</div>
<div class="flex-container canvas-tools">
<div class="flex-group brush-tools">
<label for="brush-color">颜色:</label>
<select id="brush-color">

View File

@@ -93,6 +93,7 @@ function finishCrop() {
convertDithering();
exitCropMode();
saveToHistory(); // Save after finishing crop
};
image.src = URL.createObjectURL(imageFile.files[0]);
}

View File

@@ -485,6 +485,7 @@ function updateImage() {
redrawTextElements();
redrawLineSegments();
convertDithering();
saveToHistory(); // Save after loading image
} else {
alert("图片宽高比例与画布不匹配,将进入裁剪模式。\n请放大图片后移动图片使其充满画布再点击“完成”按钮。");
setActiveTool(null, '');
@@ -531,6 +532,7 @@ function clearCanvas() {
textElements = []; // Clear stored text positions
lineSegments = []; // Clear stored line segments
if (isCropMode()) exitCropMode();
saveToHistory(); // Save cleared canvas to history
return true;
}
return false;

View File

@@ -15,6 +15,11 @@ let dragOffsetY = 0;
let textBold = false; // Track if text should be bold
let textItalic = false; // Track if text should be italic
// Undo/Redo functionality
let historyStack = []; // Stack to store canvas history
let historyStep = -1; // Current position in history stack
const MAX_HISTORY = 50; // Maximum number of undo steps
function setCanvasTitle(title) {
const canvasTitle = document.querySelector('.canvas-title');
if (canvasTitle) {
@@ -23,6 +28,71 @@ function setCanvasTitle(title) {
}
}
function saveToHistory() {
// Remove any states after current step (when user drew something after undoing)
historyStack = historyStack.slice(0, historyStep + 1);
// Save current canvas state along with text and line data
const canvasState = {
imageData: ctx.getImageData(0, 0, canvas.width, canvas.height),
textElements: JSON.parse(JSON.stringify(textElements)),
lineSegments: JSON.parse(JSON.stringify(lineSegments))
};
historyStack.push(canvasState);
historyStep++;
// Limit history size
if (historyStack.length > MAX_HISTORY) {
historyStack.shift();
historyStep--;
}
updateUndoRedoButtons();
}
function undo() {
if (historyStep > 0) {
historyStep--;
restoreFromHistory();
}
}
function redo() {
if (historyStep < historyStack.length - 1) {
historyStep++;
restoreFromHistory();
}
}
function restoreFromHistory() {
if (historyStep >= 0 && historyStep < historyStack.length) {
const state = historyStack[historyStep];
// Restore canvas image
ctx.putImageData(state.imageData, 0, 0);
// Restore text and line data
textElements = JSON.parse(JSON.stringify(state.textElements));
lineSegments = JSON.parse(JSON.stringify(state.lineSegments));
updateUndoRedoButtons();
}
}
function updateUndoRedoButtons() {
const undoBtn = document.getElementById('undo-btn');
const redoBtn = document.getElementById('redo-btn');
if (undoBtn) {
undoBtn.disabled = historyStep <= 0;
}
if (redoBtn) {
redoBtn.disabled = historyStep >= historyStack.length - 1;
}
}
function initPaintTools() {
document.getElementById('brush-mode').addEventListener('click', () => {
if (currentTool === 'brush') {
@@ -71,6 +141,10 @@ function initPaintTools() {
textItalic = !textItalic;
document.getElementById('text-italic').classList.toggle('primary', textItalic);
});
// Add undo/redo button listeners
document.getElementById('undo-btn').addEventListener('click', undo);
document.getElementById('redo-btn').addEventListener('click', redo);
canvas.addEventListener('mousedown', startPaint);
canvas.addEventListener('mousemove', paint);
@@ -82,6 +156,23 @@ function initPaintTools() {
canvas.addEventListener('touchstart', onTouchStart);
canvas.addEventListener('touchmove', onTouchMove);
canvas.addEventListener('touchend', onTouchEnd);
// Keyboard shortcuts for undo/redo
document.addEventListener('keydown', (e) => {
// Ctrl+Z or Cmd+Z for undo
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
undo();
}
// Ctrl+Y or Ctrl+Shift+Z or Cmd+Shift+Z for redo
else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
e.preventDefault();
redo();
}
});
// Initialize history with blank canvas state
saveToHistory();
}
function setActiveTool(tool, title) {
@@ -99,6 +190,9 @@ function setActiveTool(tool, title) {
document.getElementById('brush-color').disabled = currentTool === 'eraser';
document.getElementById('brush-size').disabled = currentTool === 'text';
document.getElementById('undo-btn').classList.toggle('hide', currentTool === null);
document.getElementById('redo-btn').classList.toggle('hide', currentTool === null);
// Cancel any pending text placement
cancelTextPlacement();
}
@@ -131,6 +225,9 @@ function startPaint(e) {
}
function endPaint() {
if (painting || isDraggingText) {
saveToHistory(); // Save state after drawing or dragging text
}
painting = false;
isDraggingText = false;
lastX = 0;
@@ -364,6 +461,9 @@ function placeText(e) {
ctx.fillStyle = newText.color;
ctx.fillText(newText.text, newText.x, newText.y);
// Save to history after placing text
saveToHistory();
// Reset
document.getElementById('text-input').value = '';
isTextPlacementMode = false;