1 Commits

4 changed files with 410 additions and 107 deletions

12
app.py
View File

@@ -1,6 +1,7 @@
"""
图片编辑器 - Image Editor
图片编辑器 - Image Editor v1.1.0
前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等
新增:撤销/恢复功能、合并图片顺序调整、尺寸自定义
端口: 19018
"""
@@ -28,7 +29,7 @@ def index():
@app.route('/api/health')
def health():
"""健康检查"""
return jsonify({'status': 'ok', 'time': datetime.now().isoformat()})
return jsonify({'status': 'ok', 'version': '1.1.0', 'time': datetime.now().isoformat()})
@app.route('/api/save', methods=['POST'])
def save_image():
@@ -79,7 +80,12 @@ def list_images():
if __name__ == '__main__':
print("=" * 50)
print("图片编辑器 - Image Editor")
print("图片编辑器 - Image Editor v1.1.0")
print("=" * 50)
print("新增功能:")
print(" - 撤销/恢复操作")
print(" - 合并图片顺序拖拽调整")
print(" - 合并图片尺寸自定义")
print("=" * 50)
print(f"访问地址: http://localhost:19018")
print("=" * 50)

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片编辑器 - Image Editor</title>
<title>图片编辑器 - Image Editor v1.1.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
@@ -30,16 +30,6 @@
background: #3b82f6;
color: white;
}
.image-item {
cursor: move;
position: absolute;
}
.selection-box {
border: 2px solid #3b82f6;
background: rgba(59, 130, 246, 0.1);
position: absolute;
pointer-events: none;
}
.modal {
display: none;
position: fixed;
@@ -59,8 +49,10 @@
background: white;
padding: 20px;
border-radius: 10px;
max-width: 500px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
input[type="file"] {
display: none;
@@ -75,6 +67,26 @@
.drop-zone:hover {
background: rgba(59, 130, 246, 0.1);
}
.image-order-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f3f4f6;
border-radius: 8px;
margin-bottom: 4px;
}
.image-order-item:hover {
background: #e5e7eb;
}
.drag-handle {
cursor: grab;
padding: 4px;
}
.drag-handle:hover {
background: #d1d5db;
border-radius: 4px;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
@@ -86,6 +98,13 @@
<i class="ri-image-edit-line mr-2"></i>图片编辑器
</h1>
<div class="flex gap-2">
<!-- 撤销/恢复按钮 -->
<button onclick="undo()" id="undoBtn" class="px-3 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300 disabled:opacity-50" disabled>
<i class="ri-arrow-go-back-line"></i>
</button>
<button onclick="redo()" id="redoBtn" class="px-3 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300 disabled:opacity-50" disabled>
<i class="ri-arrow-go-forward-line"></i>
</button>
<button onclick="saveImage()" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600">
<i class="ri-save-line mr-1"></i>保存
</button>
@@ -145,14 +164,14 @@
<!-- 图片列表 -->
<div id="imageList" class="mt-4 hidden">
<h3 class="text-sm font-medium text-gray-600 mb-2">已加载图片</h3>
<h3 class="text-sm font-medium text-gray-600 mb-2">已加载图片 <span id="imageCount">(0张)</span></h3>
<div id="imageItems" class="flex gap-2 overflow-x-auto pb-2"></div>
</div>
</div>
<!-- 操作历史 -->
<div class="bg-white rounded-lg shadow-sm p-4 mt-4">
<h3 class="text-sm font-medium text-gray-600 mb-2">操作历史</h3>
<h3 class="text-sm font-medium text-gray-600 mb-2">操作历史 <span id="historyInfo">(当前: 0/0)</span></h3>
<div id="historyList" class="flex gap-2 overflow-x-auto pb-2">
<span class="text-gray-400 text-sm">暂无操作</span>
</div>
@@ -162,11 +181,39 @@
<!-- 隐藏的文件输入 -->
<input type="file" id="fileInput" accept="image/*" multiple onchange="handleFileSelect(event)">
<!-- 合并图片模态框 -->
<!-- 合并图片模态框 (增强版) -->
<div id="mergeModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-stack-line mr-2"></i>合并图片</h2>
<div class="space-y-4">
<!-- 图片顺序调整 -->
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">图片顺序(拖动调整)</label>
<div id="mergeImageOrder" class="border rounded-lg p-2 min-h-100">
<p class="text-gray-400 text-sm">请先上传图片</p>
</div>
</div>
<!-- 每张图片的尺寸设置 -->
<div id="mergeSizeSettings" class="hidden">
<label class="block text-sm font-medium text-gray-600 mb-2">各图片尺寸设置</label>
<div id="mergeSizeList" class="space-y-2"></div>
<div class="mt-2 flex items-center gap-2">
<input type="checkbox" id="mergeUniformSize" checked class="rounded">
<span class="text-sm">统一尺寸(所有图片使用相同尺寸)</span>
</div>
<div id="uniformSizeSettings" class="mt-2 grid grid-cols-2 gap-2">
<div>
<label class="block text-xs text-gray-500">宽度</label>
<input type="number" id="mergeUniformWidth" class="w-full border rounded px-2 py-1" placeholder="自动">
</div>
<div>
<label class="block text-xs text-gray-500">高度</label>
<input type="number" id="mergeUniformHeight" class="w-full border rounded px-2 py-1" placeholder="自动">
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">合并方式</label>
<select id="mergeDirection" class="w-full border rounded-lg px-3 py-2">
@@ -425,11 +472,14 @@
// 全局状态
const state = {
images: [],
history: [],
historyStack: [], // 撤销栈
redoStack: [], // 恢复栈
currentTool: null,
isSelecting: false,
selection: null,
textStyles: { bold: false, italic: false }
textStyles: { bold: false, italic: false },
mergeImageOrder: [], // 合并时的图片顺序
mergeImageSizes: {} // 合并时各图片尺寸
};
// Canvas 相关
@@ -451,6 +501,135 @@
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 保存状态到历史栈
function saveState(actionName) {
// 保存当前画布状态
const imageData = canvas.toDataURL('image/png');
state.historyStack.push({
imageData: imageData,
images: JSON.parse(JSON.stringify(state.images.map(img => ({
name: img.name,
x: img.x,
y: img.y,
width: img.width,
height: img.height,
dataUrl: img.dataUrl
})))),
canvasWidth: canvas.width,
canvasHeight: canvas.height,
action: actionName,
time: new Date().toLocaleTimeString()
});
// 清空恢复栈
state.redoStack = [];
updateUndoRedoButtons();
addHistory(actionName);
}
// 撤销
function undo() {
if (state.historyStack.length === 0) return;
// 当前状态存入恢复栈
const currentData = canvas.toDataURL('image/png');
state.redoStack.push({
imageData: currentData,
images: JSON.parse(JSON.stringify(state.images.map(img => ({
name: img.name,
x: img.x,
y: img.y,
width: img.width,
height: img.height,
dataUrl: img.dataUrl
})))),
canvasWidth: canvas.width,
canvasHeight: canvas.height
});
// 恢复上一个状态
const prevState = state.historyStack.pop();
restoreState(prevState);
updateUndoRedoButtons();
addHistory('撤销: ' + prevState.action);
}
// 恢复
function redo() {
if (state.redoStack.length === 0) return;
// 当前状态存入历史栈
const currentData = canvas.toDataURL('image/png');
state.historyStack.push({
imageData: currentData,
images: JSON.parse(JSON.stringify(state.images.map(img => ({
name: img.name,
x: img.x,
y: img.y,
width: img.width,
height: img.height,
dataUrl: img.dataUrl
})))),
canvasWidth: canvas.width,
canvasHeight: canvas.height,
action: '恢复操作'
});
// 恢复下一个状态
const nextState = state.redoStack.pop();
restoreState(nextState);
updateUndoRedoButtons();
addHistory('恢复操作');
}
// 恢复状态
function restoreState(savedState) {
canvas.width = savedState.canvasWidth;
canvas.height = savedState.canvasHeight;
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
// 恢复图片列表
state.images = savedState.images.map(item => {
const i = new Image();
i.src = item.dataUrl;
return {
img: i,
name: item.name,
x: item.x,
y: item.y,
width: item.width,
height: item.height,
dataUrl: item.dataUrl
};
});
updateImageList();
if (state.images.length > 0) {
hideEmptyHint();
} else {
showEmptyHint();
}
};
img.src = savedState.imageData;
}
// 更新撤销/恢复按钮状态
function updateUndoRedoButtons() {
document.getElementById('undoBtn').disabled = state.historyStack.length === 0;
document.getElementById('redoBtn').disabled = state.redoStack.length === 0;
document.getElementById('historyInfo').textContent =
`(当前: ${state.historyStack.length}/${state.historyStack.length + state.redoStack.length})`;
}
// 设置拖放
function setupDragDrop() {
container.addEventListener('dragover', (e) => {
@@ -502,7 +681,8 @@
x: 0,
y: 0,
width: img.width,
height: img.height
height: img.height,
dataUrl: dataUrl
});
// 调整画布大小以适应图片
@@ -514,7 +694,7 @@
drawCanvas();
updateImageList();
hideEmptyHint();
addHistory('加载图片: ' + name);
saveState('加载图片: ' + name);
};
img.src = dataUrl;
}
@@ -544,6 +724,13 @@
const list = document.getElementById('imageList');
const items = document.getElementById('imageItems');
document.getElementById('imageCount').textContent = `(${state.images.length}张)`;
if (state.images.length === 0) {
list.classList.add('hidden');
return;
}
list.classList.remove('hidden');
items.innerHTML = '';
@@ -552,6 +739,7 @@
div.className = 'flex items-center gap-2 bg-gray-100 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-200';
div.innerHTML = `
<span class="text-sm truncate max-w-150">${item.name}</span>
<span class="text-xs text-gray-500">${item.width}×${item.height}</span>
<button onclick="removeImage(${index})" class="text-red-500 hover:text-red-600">
<i class="ri-close-line"></i>
</button>
@@ -565,7 +753,7 @@
state.images.splice(index, 1);
drawCanvas();
updateImageList();
addHistory('移除图片');
saveState('移除图片');
if (state.images.length === 0) {
showEmptyHint();
@@ -586,24 +774,16 @@
// 添加历史记录
function addHistory(action) {
state.history.push({
action: action,
time: new Date().toLocaleTimeString()
});
updateHistoryList();
}
// 更新历史列表
function updateHistoryList() {
const list = document.getElementById('historyList');
list.innerHTML = '';
const span = document.createElement('span');
span.className = 'text-xs bg-blue-100 text-blue-700 rounded px-2 py-1';
span.textContent = action;
state.history.slice(-10).reverse().forEach(item => {
const span = document.createElement('span');
span.className = 'text-xs bg-gray-100 rounded px-2 py-1';
span.textContent = item.action + ' (' + item.time + ')';
list.appendChild(span);
});
if (list.querySelector('.text-gray-400')) {
list.innerHTML = '';
}
list.appendChild(span);
}
// 显示模态框
@@ -616,37 +796,186 @@
document.getElementById(id).classList.remove('show');
}
// === 功能实现 ===
// === 合并图片功能 (增强版) ===
// 合并图片
function showMergeModal() {
if (state.images.length < 2) {
alert('请先上传至少2张图片');
return;
}
// 初始化合并顺序(复制当前图片顺序)
state.mergeImageOrder = state.images.map((img, idx) => ({
index: idx,
name: img.name,
dataUrl: img.dataUrl,
width: img.width,
height: img.height
}));
// 初始化尺寸设置
state.mergeImageSizes = {};
state.mergeImageOrder.forEach(item => {
state.mergeImageSizes[item.index] = {
width: item.width,
height: item.height
};
});
renderMergeImageOrder();
renderMergeSizeList();
document.getElementById('mergeSizeSettings').classList.remove('hidden');
showModal('mergeModal');
}
// 渲染合并图片顺序列表
function renderMergeImageOrder() {
const container = document.getElementById('mergeImageOrder');
container.innerHTML = '';
state.mergeImageOrder.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'image-order-item';
div.draggable = true;
div.dataset.index = idx;
div.innerHTML = `
<span class="drag-handle"><i class="ri-draggable"></i></span>
<img src="${item.dataUrl}" class="w-12 h-12 object-cover rounded">
<span class="text-sm flex-1 truncate">${item.name}</span>
<span class="text-xs text-gray-500">${item.width}×${item.height}</span>
<span class="text-xs bg-blue-500 text-white rounded px-2 py-0.5">#${idx + 1}</span>
`;
// 拖拽事件
div.addEventListener('dragstart', handleDragStart);
div.addEventListener('dragover', handleDragOver);
div.addEventListener('drop', handleDrop);
div.addEventListener('dragend', handleDragEnd);
container.appendChild(div);
});
}
// 渲染尺寸设置列表
function renderMergeSizeList() {
const container = document.getElementById('mergeSizeList');
container.innerHTML = '';
state.mergeImageOrder.forEach((item, idx) => {
const originalIndex = item.index;
const size = state.mergeImageSizes[originalIndex] || { width: item.width, height: item.height };
const div = document.createElement('div');
div.className = 'flex items-center gap-2 text-sm';
div.innerHTML = `
<span class="truncate w-100">${item.name}</span>
<input type="number" value="${size.width}"
onchange="updateMergeSize(${originalIndex}, 'width', this.value)"
class="border rounded px-2 py-1 w-16" placeholder="宽">
<input type="number" value="${size.height}"
onchange="updateMergeSize(${originalIndex}, 'height', this.value)"
class="border rounded px-2 py-1 w-16" placeholder="高">
`;
container.appendChild(div);
});
}
// 更新合并尺寸
function updateMergeSize(index, dimension, value) {
state.mergeImageSizes[index][dimension] = parseInt(value) || 0;
}
// 拖拽处理
let draggedItem = null;
function handleDragStart(e) {
draggedItem = this;
this.style.opacity = '0.5';
}
function handleDragOver(e) {
e.preventDefault();
}
function handleDrop(e) {
e.preventDefault();
if (draggedItem !== this) {
const fromIdx = parseInt(draggedItem.dataset.index);
const toIdx = parseInt(this.dataset.index);
// 交换顺序
const temp = state.mergeImageOrder[fromIdx];
state.mergeImageOrder[fromIdx] = state.mergeImageOrder[toIdx];
state.mergeImageOrder[toIdx] = temp;
renderMergeImageOrder();
renderMergeSizeList();
}
}
function handleDragEnd(e) {
this.style.opacity = '1';
draggedItem = null;
}
// 监听统一尺寸选项
document.getElementById('mergeUniformSize').addEventListener('change', (e) => {
document.getElementById('uniformSizeSettings').classList.toggle('hidden', !e.target.checked);
document.getElementById('mergeSizeList').classList.toggle('hidden', e.target.checked);
});
// 监听合并方式变化
document.getElementById('mergeDirection').addEventListener('change', (e) => {
document.getElementById('gridSettings').classList.toggle('hidden', e.target.value !== 'grid');
});
function applyMerge() {
const direction = document.getElementById('mergeDirection').value;
const gap = parseInt(document.getElementById('mergeGap').value) || 0;
const bgColor = document.getElementById('mergeBgColor').value;
const uniformSize = document.getElementById('mergeUniformSize').checked;
const uniformWidth = parseInt(document.getElementById('mergeUniformWidth').value) || 0;
const uniformHeight = parseInt(document.getElementById('mergeUniformHeight').value) || 0;
let cols = 1, rows = state.images.length;
let cols = 1, rows = state.mergeImageOrder.length;
if (direction === 'horizontal') {
cols = state.images.length;
cols = state.mergeImageOrder.length;
rows = 1;
} else if (direction === 'grid') {
cols = parseInt(document.getElementById('gridCols').value) || 2;
rows = parseInt(document.getElementById('gridRows').value) || 2;
}
// 计算合并后的尺寸
// 获取每张图片的实际尺寸
const getActualSize = (item) => {
if (uniformSize && uniformWidth && uniformHeight) {
return { width: uniformWidth, height: uniformHeight };
}
if (uniformSize) {
// 使用最大尺寸
const maxWidth = Math.max(...state.mergeImageOrder.map(i => i.width));
const maxHeight = Math.max(...state.mergeImageOrder.map(i => i.height));
return { width: uniformWidth || maxWidth, height: uniformHeight || maxHeight };
}
const size = state.mergeImageSizes[item.index] || { width: item.width, height: item.height };
return {
width: size.width || item.width,
height: size.height || item.height
};
};
// 计算最大单元格尺寸
let maxWidth = 0, maxHeight = 0;
state.images.forEach(item => {
maxWidth = Math.max(maxWidth, item.width);
maxHeight = Math.max(maxHeight, item.height);
state.mergeImageOrder.forEach(item => {
const size = getActualSize(item);
maxWidth = Math.max(maxWidth, size.width);
maxHeight = Math.max(maxHeight, size.height);
});
const totalWidth = cols * maxWidth + (cols - 1) * gap;
@@ -659,18 +988,25 @@
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, totalWidth, totalHeight);
// 绘制图片
state.images.forEach((item, index) => {
// 按顺序绘制图片
state.mergeImageOrder.forEach((orderItem, index) => {
if (index >= cols * rows) return; // 超出网格容量
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * (maxWidth + gap);
const y = row * (maxHeight + gap);
// 居中绘制
const offsetX = (maxWidth - item.width) / 2;
const offsetY = (maxHeight - item.height) / 2;
const size = getActualSize(orderItem);
ctx.drawImage(item.img, x + offsetX, y + offsetY, item.width, item.height);
// 居中绘制
const offsetX = (maxWidth - size.width) / 2;
const offsetY = (maxHeight - size.height) / 2;
const originalImg = state.images.find(img => img.name === orderItem.name);
if (originalImg) {
ctx.drawImage(originalImg.img, x + offsetX, y + offsetY, size.width, size.height);
}
});
// 清空图片列表(合并后为单张)
@@ -679,14 +1015,9 @@
loadImage(mergedData, 'merged_' + new Date().getTime());
closeModal('mergeModal');
addHistory('合并图片 (' + direction + ')');
saveState('合并图片 (' + direction + ')');
}
// 监听合并方式变化
document.getElementById('mergeDirection').addEventListener('change', (e) => {
document.getElementById('gridSettings').classList.toggle('hidden', e.target.value !== 'grid');
});
// 分割图片
function showSplitModal() {
if (state.images.length === 0) {
@@ -695,7 +1026,6 @@
}
showModal('splitModal');
// 设置默认尺寸
const img = state.images[0];
document.getElementById('cropWidth').value = img.width;
document.getElementById('cropHeight').value = img.height;
@@ -727,7 +1057,6 @@
const dataUrl = pieceCanvas.toDataURL('image/png');
// 显示分割结果
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
@@ -743,10 +1072,9 @@
document.getElementById('splitResults').classList.remove('hidden');
closeModal('splitModal');
addHistory(`分割图片 (${cols}x${rows})`);
saveState(`分割图片 (${cols}x${rows})`);
}
// 下载分割片段
function downloadSplitPiece(dataUrl, filename) {
const link = document.createElement('a');
link.href = dataUrl;
@@ -763,13 +1091,9 @@
showModal('holeModal');
}
// 监听填充方式变化
document.getElementById('holeFill').addEventListener('change', (e) => {
const colorPicker = document.getElementById('holeColorPicker');
const blurSetting = document.getElementById('holeBlurSetting');
colorPicker.classList.toggle('hidden', e.target.value !== 'color');
blurSetting.classList.toggle('hidden', e.target.value !== 'blur');
document.getElementById('holeColorPicker').classList.toggle('hidden', e.target.value !== 'color');
document.getElementById('holeBlurSetting').classList.toggle('hidden', e.target.value !== 'blur');
});
function startHoleSelection() {
@@ -781,7 +1105,6 @@
alert('在画布上拖动选择要挖孔的区域');
}
// 设置选择监听
function setupSelectionListeners() {
let startX, startY;
@@ -815,7 +1138,6 @@
if (state.currentTool === 'hole') {
applyHole();
} else if (state.currentTool === 'crop') {
// 更新裁剪坐标
document.getElementById('cropX').value = Math.round(state.selection.x);
document.getElementById('cropY').value = Math.round(state.selection.y);
document.getElementById('cropWidth').value = Math.round(Math.abs(state.selection.width));
@@ -865,20 +1187,16 @@
} else if (fill === 'transparent') {
ctx.clearRect(x, y, w, h);
} else if (fill === 'blur') {
// 获取区域图像并模糊
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(canvas, x, y, w, h, 0, 0, w, h);
// 应用模糊(简单近似)
ctx.filter = `blur(${blurRadius}px)`;
ctx.drawImage(tempCanvas, x, y);
ctx.filter = 'none';
} else if (fill === 'content-aware') {
// 智能填充:取边缘颜色延伸
// 获取边缘像素
const edgeColor = getEdgeColor(x, y, w, h);
ctx.fillStyle = edgeColor;
if (shape === 'rectangle') {
@@ -890,12 +1208,10 @@
}
}
addHistory('挖孔填充 (' + shape + ')');
saveState('挖孔填充 (' + shape + ')');
}
// 获取边缘颜色
function getEdgeColor(x, y, w, h) {
// 取左上角附近像素
const pixel = ctx.getImageData(Math.max(0, x - 1), Math.max(0, y - 1), 1, 1).data;
return `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;
}
@@ -909,7 +1225,6 @@
showModal('circleModal');
}
// 监听边缘处理变化
document.getElementById('circleEdge').addEventListener('change', (e) => {
document.getElementById('circleBorderSetting').classList.toggle('hidden', e.target.value !== 'border');
});
@@ -923,19 +1238,16 @@
const img = state.images[0];
const size = diameter ? parseInt(diameter) : Math.min(img.width, img.height);
// 创建圆形画布
const circleCanvas = document.createElement('canvas');
circleCanvas.width = size;
circleCanvas.height = size;
const circleCtx = circleCanvas.getContext('2d');
// 绘制圆形
circleCtx.beginPath();
circleCtx.arc(size/2, size/2, size/2 - (edge === 'border' ? borderWidth : 0), 0, Math.PI * 2);
circleCtx.closePath();
if (edge === 'smooth') {
// 添加平滑边缘(半透明渐变)
circleCtx.globalCompositeOperation = 'source-over';
const gradient = circleCtx.createRadialGradient(size/2, size/2, size/2 - 10, size/2, size/2, size/2);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
@@ -944,15 +1256,12 @@
circleCtx.fill();
}
// 剪切区域
circleCtx.clip();
// 绘制图片(居中)
const offsetX = (img.width - size) / 2;
const offsetY = (img.height - size) / 2;
circleCtx.drawImage(img.img, -offsetX, -offsetY);
// 添加边框
if (edge === 'border') {
circleCtx.strokeStyle = borderColor;
circleCtx.lineWidth = borderWidth;
@@ -961,18 +1270,16 @@
circleCtx.stroke();
}
// 更新画布
canvas.width = size;
canvas.height = size;
ctx.drawImage(circleCanvas, 0, 0);
// 更新状态
const circleData = canvas.toDataURL('image/png');
state.images = [];
loadImage(circleData, 'circle_' + new Date().getTime());
closeModal('circleModal');
addHistory('圆形切图 (直径: ' + size + ')');
saveState('圆形切图 (直径: ' + size + ')');
}
// 文字图片
@@ -1001,12 +1308,10 @@
const bgColor = document.getElementById('textBgColor').value;
const align = document.getElementById('textAlign').value;
// 计算文字尺寸
const fontStyle = (state.textStyles.bold ? 'bold ' : '') +
(state.textStyles.italic ? 'italic ' : '') +
size + 'px ' + font;
// 创建临时画布测量文字
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCtx.font = fontStyle;
@@ -1022,11 +1327,9 @@
const totalHeight = lines.length * lineHeight + padding * 2;
const totalWidth = maxWidth + padding * 2;
// 创建文字画布
canvas.width = totalWidth;
canvas.height = totalHeight;
// 绘制背景
if (!transparentBg) {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, totalWidth, totalHeight);
@@ -1034,7 +1337,6 @@
ctx.clearRect(0, 0, totalWidth, totalHeight);
}
// 绘制文字
ctx.font = fontStyle;
ctx.fillStyle = color;
ctx.textBaseline = 'top';
@@ -1048,13 +1350,12 @@
ctx.fillText(line, x, y);
});
// 更新状态
const textData = canvas.toDataURL('image/png');
state.images = [];
loadImage(textData, 'text_' + new Date().getTime());
closeModal('textModal');
addHistory('创建文字图片');
saveState('创建文字图片');
}
// 调整大小
@@ -1074,7 +1375,6 @@
function applyResize() {
const newWidth = parseInt(document.getElementById('newWidth').value);
const newHeight = parseInt(document.getElementById('newHeight').value);
const keepRatio = document.getElementById('keepRatio').checked;
if (!newWidth || !newHeight) {
alert('请输入有效的尺寸');
@@ -1083,19 +1383,17 @@
const img = state.images[0];
// 创建缩放后的画布
canvas.width = newWidth;
canvas.height = newHeight;
ctx.drawImage(img.img, 0, 0, newWidth, newHeight);
// 更新状态
const resizedData = canvas.toDataURL('image/png');
state.images = [];
loadImage(resizedData, 'resized_' + new Date().getTime());
closeModal('resizeModal');
addHistory(`调整大小 (${newWidth}x${newHeight})`);
saveState(`调整大小 (${newWidth}x${newHeight})`);
}
// 裁剪
@@ -1129,19 +1427,17 @@
const img = state.images[0];
// 创建裁剪后的画布
canvas.width = w;
canvas.height = h;
ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h);
// 更新状态
const croppedData = canvas.toDataURL('image/png');
state.images = [];
loadImage(croppedData, 'cropped_' + new Date().getTime());
closeModal('cropModal');
addHistory(`裁剪 (${w}x${h})`);
saveState(`裁剪 (${w}x${h})`);
}
// 保存图片
@@ -1158,7 +1454,7 @@
.then(data => {
if (data.success) {
alert('保存成功: ' + data.filename);
addHistory('保存图片: ' + data.filename);
saveState('保存图片: ' + data.filename);
} else {
alert('保存失败');
}
@@ -1172,19 +1468,20 @@
link.href = dataUrl;
link.download = 'edited_image_' + new Date().getTime() + '.png';
link.click();
addHistory('下载图片');
saveState('下载图片');
}
// 清空画布
function clearCanvas() {
state.images = [];
state.history = [];
state.historyStack = [];
state.redoStack = [];
setupCanvas();
updateImageList();
updateHistoryList();
updateUndoRedoButtons();
document.getElementById('historyList').innerHTML = '<span class="text-gray-400 text-sm">暂无操作</span>';
showEmptyHint();
document.getElementById('splitResults').classList.add('hidden');
addHistory('清空画布');
}
// 初始化