Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5e4962050 |
12
app.py
12
app.py
@@ -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)
|
||||
|
||||
BIN
outputs/edited_1776739175870.png
Normal file
BIN
outputs/edited_1776739175870.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 702 KiB |
BIN
outputs/edited_1776739272589.png
Normal file
BIN
outputs/edited_1776739272589.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -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('清空画布');
|
||||
}
|
||||
|
||||
// 初始化
|
||||
|
||||
Reference in New Issue
Block a user