Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f720f02d2 | |||
| a99cb57a2f | |||
| 6db10c0c83 | |||
| 8bdd18bdcf | |||
| 1bf6d6448c | |||
| 7bde3ce551 | |||
| 7ad0538039 | |||
| 5aa802e960 |
10
app.py
10
app.py
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
图片编辑器 - Image Editor v1.2.0
|
||||
图片编辑器 - Image Editor v1.2.7
|
||||
前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等
|
||||
v1.2.0: 右侧面板实时配置,无需弹框
|
||||
v1.2.7: 修复合并图片紧密拼接问题
|
||||
|
||||
端口: 19018
|
||||
"""
|
||||
@@ -25,7 +25,7 @@ def index():
|
||||
|
||||
@app.route('/api/health')
|
||||
def health():
|
||||
return jsonify({'status': 'ok', 'version': '1.2.0', 'time': datetime.now().isoformat()})
|
||||
return jsonify({'status': 'ok', 'version': '1.2.7', 'time': datetime.now().isoformat()})
|
||||
|
||||
@app.route('/api/save', methods=['POST'])
|
||||
def save_image():
|
||||
@@ -64,9 +64,9 @@ def list_images():
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("图片编辑器 - Image Editor v1.2.0")
|
||||
print("图片编辑器 - Image Editor v1.2.7")
|
||||
print("=" * 50)
|
||||
print("右侧面板实时配置,操作更流畅")
|
||||
print("修复合并图片紧密拼接问题")
|
||||
print(f"访问地址: http://localhost:19018")
|
||||
print("=" * 50)
|
||||
app.run(host='0.0.0.0', port=19018, debug=True)
|
||||
BIN
outputs/edited_1776755761871.png
Normal file
BIN
outputs/edited_1776755761871.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 763 KiB |
BIN
outputs/edited_1776761087118.png
Normal file
BIN
outputs/edited_1776761087118.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 763 KiB |
BIN
outputs/edited_1776778260696.png
Normal file
BIN
outputs/edited_1776778260696.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 768 KiB |
BIN
outputs/edited_1776778342793.png
Normal file
BIN
outputs/edited_1776778342793.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 767 KiB |
BIN
outputs/edited_1776778549740.png
Normal file
BIN
outputs/edited_1776778549740.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 796 KiB |
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>图片编辑器 - Image Editor v1.2.0</title>
|
||||
<title>图片编辑器 - Image Editor v1.2.7</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>
|
||||
@@ -90,9 +90,6 @@
|
||||
|
||||
<!-- 功能按钮 -->
|
||||
<div class="mt-3 flex gap-2 flex-wrap">
|
||||
<button onclick="setTool('upload')" id="tool-upload" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
|
||||
<i class="ri-upload-cloud-line mr-1"></i>上传图片
|
||||
</button>
|
||||
<button onclick="setTool('merge')" id="tool-merge" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
|
||||
<i class="ri-stack-line mr-1"></i>合并图片
|
||||
</button>
|
||||
@@ -127,7 +124,8 @@
|
||||
<canvas id="mainCanvas"></canvas>
|
||||
<div id="emptyHint" class="text-gray-400 text-center">
|
||||
<i class="ri-image-add-line text-6xl"></i>
|
||||
<p class="mt-2">点击"上传图片"或拖放图片开始编辑</p>
|
||||
<p class="mt-2">拖动图片到此处上传</p>
|
||||
<p class="text-sm mt-1">支持多张图片同时上传</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,14 +143,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧配置面板 -->
|
||||
<div id="rightPanel" class="right-panel hidden-panel w-80 bg-white shadow-lg border-l">
|
||||
<!-- 右侧配置面板 - 始终显示 -->
|
||||
<div id="rightPanel" class="right-panel w-80 bg-white shadow-lg border-l">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 id="panelTitle" class="font-bold text-gray-800">配置</h2>
|
||||
<button onclick="closePanel()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="ri-close-line text-xl"></i>
|
||||
</button>
|
||||
<h2 id="panelTitle" class="font-bold text-gray-800">操作面板</h2>
|
||||
</div>
|
||||
|
||||
<!-- 配置内容区域 -->
|
||||
@@ -188,17 +183,158 @@
|
||||
selection: null,
|
||||
textStyles: { bold: false, italic: false },
|
||||
mergeImageOrder: [],
|
||||
mergeImageSizes: {}
|
||||
mergeImageSizes: {},
|
||||
previewMode: false, // 预览模式
|
||||
previewData: null, // 预览图片数据
|
||||
originalImages: null // 预览前保存的原始图片
|
||||
};
|
||||
|
||||
const canvas = document.getElementById('mainCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const container = document.getElementById('canvasContainer');
|
||||
|
||||
// 检查是否有图片(画布有内容)
|
||||
function hasImage() {
|
||||
return state.images.length > 0 || document.getElementById('emptyHint').classList.contains('hidden');
|
||||
}
|
||||
// 显示预览效果(操作后显示结果,但保留原始图片)
|
||||
function showPreviewResult(dataUrl, name) {
|
||||
// 进入预览模式
|
||||
state.previewMode = true;
|
||||
state.previewData = dataUrl;
|
||||
|
||||
// 保存原始图片状态
|
||||
state.originalImages = state.images.map(img => ({
|
||||
...img,
|
||||
// 不复制img对象,只保存数据
|
||||
}));
|
||||
|
||||
// 显示预览效果(追加结果图片,不清除原始)
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// 调整画布大小以适应结果图片
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
// 绘制结果图片
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
hideEmptyHint();
|
||||
|
||||
// 显示预览面板
|
||||
showPreviewPanel(name);
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
// 显示预览面板
|
||||
function showPreviewPanel(operationName) {
|
||||
document.getElementById('panelTitle').textContent = '预览效果';
|
||||
document.getElementById('panelContent').innerHTML = `
|
||||
<div class="space-y-3">
|
||||
<div class="p-3 bg-green-50 rounded-lg">
|
||||
<p class="text-sm text-green-700">
|
||||
<i class="ri-check-line"></i> ${operationName} 完成!
|
||||
</p>
|
||||
<p class="text-xs text-green-600 mt-1">预览效果已显示,原图片仍保留</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">选择"应用结果"替换原图,或"取消"恢复</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('panelActions').innerHTML = `
|
||||
<button onclick="applyPreviewResult()" class="px-4 py-2 bg-green-500 text-white rounded-lg w-full mb-2">
|
||||
<i class="ri-check-double-line mr-1"></i>应用结果
|
||||
</button>
|
||||
<button onclick="cancelPreview()" class="px-4 py-2 bg-gray-200 rounded-lg w-full mb-2">
|
||||
<i class="ri-close-line mr-1"></i>取消预览
|
||||
</button>
|
||||
<button onclick="goHome()" class="px-4 py-2 bg-blue-100 text-blue-600 rounded-lg w-full">
|
||||
<i class="ri-home-line mr-1"></i>返回首页
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 应用预览结果
|
||||
function applyPreviewResult() {
|
||||
if (!state.previewData) return;
|
||||
|
||||
// 清空原始图片,只保留结果
|
||||
state.images = [];
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
state.images.push({
|
||||
img: img,
|
||||
name: 'result_' + Date.now(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
dataUrl: state.previewData
|
||||
});
|
||||
|
||||
drawCanvas();
|
||||
updateImageList();
|
||||
saveState('应用结果');
|
||||
|
||||
// 清除预览状态
|
||||
state.previewMode = false;
|
||||
state.previewData = null;
|
||||
state.originalImages = null;
|
||||
|
||||
// 返回首页
|
||||
goHome();
|
||||
};
|
||||
img.src = state.previewData;
|
||||
}
|
||||
|
||||
// 取消预览
|
||||
function cancelPreview() {
|
||||
if (!state.originalImages) {
|
||||
goHome();
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复原始图片
|
||||
state.images = [];
|
||||
state.originalImages.forEach((item, idx) => {
|
||||
if (state.images[idx] && state.images[idx].img) {
|
||||
state.images.push(state.images[idx]);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有恢复成功,重新从画布历史恢复
|
||||
if (state.images.length === 0 && state.historyStack.length > 0) {
|
||||
const lastState = state.historyStack[state.historyStack.length - 1];
|
||||
restoreState(lastState);
|
||||
} else {
|
||||
drawCanvas();
|
||||
updateImageList();
|
||||
}
|
||||
|
||||
// 清除预览状态
|
||||
state.previewMode = false;
|
||||
state.previewData = null;
|
||||
state.originalImages = null;
|
||||
|
||||
// 返回首页
|
||||
goHome();
|
||||
}
|
||||
|
||||
// 刷新当前工具面板(执行操作后保持当前配置)
|
||||
function refreshCurrentPanel() {
|
||||
if (state.currentTool) {
|
||||
showPanel(state.currentTool);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
setupDragDrop();
|
||||
setupCanvas();
|
||||
// 显示默认首页面板
|
||||
showPanel('home');
|
||||
}
|
||||
|
||||
function setupCanvas() {
|
||||
@@ -230,18 +366,14 @@
|
||||
|
||||
state.currentTool = tool;
|
||||
|
||||
// 显示右侧配置面板
|
||||
// 更新右侧配置面板内容(不隐藏)
|
||||
showPanel(tool);
|
||||
|
||||
// 上传图片直接触发文件选择
|
||||
if (tool === 'upload') {
|
||||
document.getElementById('fileInput').click();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示右侧面板
|
||||
// 显示右侧面板内容
|
||||
function showPanel(tool) {
|
||||
const panel = document.getElementById('rightPanel');
|
||||
// 确保面板始终显示
|
||||
panel.classList.remove('hidden-panel');
|
||||
|
||||
const configs = getPanelConfig(tool);
|
||||
@@ -250,35 +382,67 @@
|
||||
document.getElementById('panelActions').innerHTML = configs.actions;
|
||||
}
|
||||
|
||||
// 关闭面板
|
||||
function closePanel() {
|
||||
const panel = document.getElementById('rightPanel');
|
||||
panel.classList.add('hidden-panel');
|
||||
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
||||
state.currentTool = null;
|
||||
}
|
||||
|
||||
// 获取各工具的配置面板内容
|
||||
function getPanelConfig(tool) {
|
||||
switch(tool) {
|
||||
case 'upload':
|
||||
<!-- 默认首页面板 -->
|
||||
if (!tool || tool === 'home') {
|
||||
return {
|
||||
title: '上传图片',
|
||||
title: '欢迎使用',
|
||||
content: `
|
||||
<p class="text-sm text-gray-600 mb-4">点击按钮选择图片,或直接拖放图片到画布区域</p>
|
||||
<button onclick="document.getElementById('fileInput').click()" class="w-full px-4 py-3 bg-blue-500 text-white rounded-lg">
|
||||
<i class="ri-upload-cloud-line mr-2"></i>选择图片
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-600">点击顶部功能按钮开始编辑图片</p>
|
||||
<div class="p-3 bg-blue-50 rounded-lg border-2 border-dashed border-blue-300 text-center">
|
||||
<i class="ri-upload-cloud-2-line text-3xl text-blue-500"></i>
|
||||
<p class="text-sm text-blue-600 mt-2">拖动图片到左侧画布区域上传</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button onclick="setTool('merge')" class="p-3 bg-green-50 rounded-lg hover:bg-green-100 text-center">
|
||||
<i class="ri-stack-line text-2xl text-green-500"></i>
|
||||
<p class="text-xs mt-1">合并图片</p>
|
||||
</button>
|
||||
<button onclick="setTool('split')" class="p-3 bg-purple-50 rounded-lg hover:bg-purple-100 text-center">
|
||||
<i class="ri-layout-grid-line text-2xl text-purple-500"></i>
|
||||
<p class="text-xs mt-1">分割图片</p>
|
||||
</button>
|
||||
<button onclick="setTool('circle')" class="p-3 bg-orange-50 rounded-lg hover:bg-orange-100 text-center">
|
||||
<i class="ri-checkbox-blank-circle-line text-2xl text-orange-500"></i>
|
||||
<p class="text-xs mt-1">圆形切图</p>
|
||||
</button>
|
||||
<button onclick="setTool('text')" class="p-3 bg-pink-50 rounded-lg hover:bg-pink-100 text-center">
|
||||
<i class="ri-font-size text-2xl text-pink-500"></i>
|
||||
<p class="text-xs mt-1">文字图片</p>
|
||||
</button>
|
||||
<button onclick="setTool('resize')" class="p-3 bg-cyan-50 rounded-lg hover:bg-cyan-100 text-center">
|
||||
<i class="ri-expand-diagonal-line text-2xl text-cyan-500"></i>
|
||||
<p class="text-xs mt-1">调整大小</p>
|
||||
</button>
|
||||
<button onclick="setTool('crop')" class="p-3 bg-yellow-50 rounded-lg hover:bg-yellow-100 text-center">
|
||||
<i class="ri-crop-line text-2xl text-yellow-500"></i>
|
||||
<p class="text-xs mt-1">裁剪</p>
|
||||
</button>
|
||||
<button onclick="setTool('hole')" class="p-3 bg-red-50 rounded-lg hover:bg-red-100 text-center">
|
||||
<i class="ri-contrast-drop-line text-2xl text-red-500"></i>
|
||||
<p class="text-xs mt-1">挖孔填充</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="border-t pt-3">
|
||||
<p class="text-xs text-gray-500">提示:所有操作都支持撤销恢复</p>
|
||||
<p class="text-xs text-gray-500 mt-1">快捷键:Ctrl+Z 撤销,Ctrl+Y 恢复</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
actions: ''
|
||||
};
|
||||
}
|
||||
|
||||
switch(tool) {
|
||||
|
||||
case 'merge':
|
||||
if (state.images.length < 2) {
|
||||
if (!hasImage() || state.images.length < 2) {
|
||||
return {
|
||||
title: '合并图片',
|
||||
content: `<p class="text-sm text-red-500">请先上传至少2张图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -351,11 +515,11 @@
|
||||
};
|
||||
|
||||
case 'split':
|
||||
if (state.images.length === 0) {
|
||||
if (!hasImage()) {
|
||||
return {
|
||||
title: '分割图片',
|
||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -382,11 +546,11 @@
|
||||
};
|
||||
|
||||
case 'hole':
|
||||
if (state.images.length === 0) {
|
||||
if (!hasImage()) {
|
||||
return {
|
||||
title: '挖孔填充',
|
||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -421,15 +585,18 @@
|
||||
<p class="text-xs text-gray-500">点击"开始选择"后,在画布上拖动选择区域</p>
|
||||
</div>
|
||||
`,
|
||||
actions: `<button onclick="startHoleSelection()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">开始选择</button>`
|
||||
actions: `
|
||||
<button onclick="startHoleSelection()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full mb-2">开始选择</button>
|
||||
<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg w-full">关闭面板</button>
|
||||
`
|
||||
};
|
||||
|
||||
case 'circle':
|
||||
if (state.images.length === 0) {
|
||||
if (!hasImage()) {
|
||||
return {
|
||||
title: '圆形切图',
|
||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -511,11 +678,11 @@
|
||||
};
|
||||
|
||||
case 'resize':
|
||||
if (state.images.length === 0) {
|
||||
if (!hasImage()) {
|
||||
return {
|
||||
title: '调整大小',
|
||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
const img = state.images[0];
|
||||
@@ -542,11 +709,11 @@
|
||||
};
|
||||
|
||||
case 'crop':
|
||||
if (state.images.length === 0) {
|
||||
if (!hasImage()) {
|
||||
return {
|
||||
title: '裁剪',
|
||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||
actions: `<button onclick="closePanel()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -812,10 +979,17 @@
|
||||
updateImageList();
|
||||
hideEmptyHint();
|
||||
saveState('加载: ' + name);
|
||||
// 刷新当前面板
|
||||
refreshCurrentPanel();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
// 加载操作结果(不清空原始图片)
|
||||
function loadOperationResult(dataUrl, name, operationName) {
|
||||
showPreviewResult(dataUrl, operationName);
|
||||
}
|
||||
|
||||
function drawCanvas() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
@@ -836,7 +1010,7 @@
|
||||
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; }
|
||||
if (!hasImage()) { list.classList.add('hidden'); return; }
|
||||
list.classList.remove('hidden');
|
||||
items.innerHTML = '';
|
||||
state.images.forEach((item, index) => {
|
||||
@@ -856,7 +1030,7 @@
|
||||
drawCanvas();
|
||||
updateImageList();
|
||||
saveState('移除图片');
|
||||
if (state.images.length === 0) showEmptyHint();
|
||||
if (!hasImage()) showEmptyHint();
|
||||
}
|
||||
|
||||
function hideEmptyHint() {
|
||||
@@ -907,19 +1081,76 @@
|
||||
return { width: size.width || item.width, height: size.height || item.height };
|
||||
};
|
||||
|
||||
// 计算画布尺寸
|
||||
let canvasWidth = 0, canvasHeight = 0;
|
||||
|
||||
if (uniformSize) {
|
||||
// 统一尺寸:使用固定单元格大小
|
||||
let maxWidth = 0, maxHeight = 0;
|
||||
state.mergeImageOrder.forEach(item => {
|
||||
const size = getActualSize(item);
|
||||
maxWidth = Math.max(maxWidth, size.width);
|
||||
maxHeight = Math.max(maxHeight, size.height);
|
||||
});
|
||||
canvasWidth = cols * maxWidth + (cols - 1) * gap;
|
||||
canvasHeight = rows * maxHeight + (rows - 1) * gap;
|
||||
} else {
|
||||
// 不统一尺寸:紧密拼接,累加实际尺寸
|
||||
if (direction === 'horizontal') {
|
||||
// 横向:累加宽度,取最大高度
|
||||
state.mergeImageOrder.forEach(item => {
|
||||
const size = getActualSize(item);
|
||||
canvasWidth += size.width + gap;
|
||||
canvasHeight = Math.max(canvasHeight, size.height);
|
||||
});
|
||||
canvasWidth -= gap; // 去掉最后一个多余的gap
|
||||
} else if (direction === 'vertical') {
|
||||
// 纵向:累加高度,取最大宽度
|
||||
state.mergeImageOrder.forEach(item => {
|
||||
const size = getActualSize(item);
|
||||
canvasHeight += size.height + gap;
|
||||
canvasWidth = Math.max(canvasWidth, size.width);
|
||||
});
|
||||
canvasHeight -= gap;
|
||||
} else {
|
||||
// 网格:每行累加宽度,累加行高度
|
||||
for (let r = 0; r < rows; r++) {
|
||||
let rowWidth = 0, rowHeight = 0;
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const idx = r * cols + c;
|
||||
if (idx < state.mergeImageOrder.length) {
|
||||
const size = getActualSize(state.mergeImageOrder[idx]);
|
||||
rowWidth += size.width + gap;
|
||||
rowHeight = Math.max(rowHeight, size.height);
|
||||
}
|
||||
}
|
||||
rowWidth -= gap;
|
||||
canvasWidth = Math.max(canvasWidth, rowWidth);
|
||||
canvasHeight += rowHeight + gap;
|
||||
}
|
||||
canvasHeight -= gap;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = cols * maxWidth + (cols - 1) * gap;
|
||||
canvas.height = rows * maxHeight + (rows - 1) * gap;
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
|
||||
// 绘制合并结果到画布(预览)
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制图片
|
||||
let currentX = 0, currentY = 0;
|
||||
|
||||
if (uniformSize) {
|
||||
// 统一尺寸:固定单元格,居中绘制
|
||||
let maxWidth = 0, maxHeight = 0;
|
||||
state.mergeImageOrder.forEach(item => {
|
||||
const size = getActualSize(item);
|
||||
maxWidth = Math.max(maxWidth, size.width);
|
||||
maxHeight = Math.max(maxHeight, size.height);
|
||||
});
|
||||
|
||||
state.mergeImageOrder.forEach((orderItem, index) => {
|
||||
if (index >= cols * rows) return;
|
||||
const col = index % cols;
|
||||
@@ -935,11 +1166,37 @@
|
||||
ctx.drawImage(originalImg.img, x + offsetX, y + offsetY, size.width, size.height);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 不统一尺寸:紧密拼接,无居中偏移
|
||||
state.mergeImageOrder.forEach((orderItem, index) => {
|
||||
const size = getActualSize(orderItem);
|
||||
const originalImg = state.images.find(img => img.name === orderItem.name);
|
||||
|
||||
if (!originalImg) return;
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
ctx.drawImage(originalImg.img, currentX, 0, size.width, size.height);
|
||||
currentX += size.width + gap;
|
||||
} else if (direction === 'vertical') {
|
||||
ctx.drawImage(originalImg.img, 0, currentY, size.width, size.height);
|
||||
currentY += size.height + gap;
|
||||
} else {
|
||||
// 网格模式
|
||||
const col = index % cols;
|
||||
const row = Math.floor(index / cols);
|
||||
if (col === 0) currentX = 0;
|
||||
ctx.drawImage(originalImg.img, currentX, currentY, size.width, size.height);
|
||||
currentX += size.width + gap;
|
||||
if (col === cols - 1 || index === state.mergeImageOrder.length - 1) {
|
||||
currentY += size.height + gap;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示预览效果(不清除原始图片)
|
||||
const mergedData = canvas.toDataURL('image/png');
|
||||
state.images = [];
|
||||
loadImage(mergedData, 'merged');
|
||||
closePanel();
|
||||
showPreviewResult(mergedData, '合并图片');
|
||||
}
|
||||
|
||||
function applySplit() {
|
||||
@@ -976,7 +1233,7 @@
|
||||
}
|
||||
|
||||
document.getElementById('splitResults').classList.remove('hidden');
|
||||
closePanel();
|
||||
refreshCurrentPanel();
|
||||
saveState(`分割 (${cols}x${rows})`);
|
||||
}
|
||||
|
||||
@@ -1115,9 +1372,7 @@
|
||||
ctx.drawImage(circleCanvas, 0, 0);
|
||||
|
||||
const circleData = canvas.toDataURL('image/png');
|
||||
state.images = [];
|
||||
loadImage(circleData, 'circle');
|
||||
closePanel();
|
||||
showPreviewResult(circleData, '圆形切图');
|
||||
}
|
||||
|
||||
function createTextImage() {
|
||||
@@ -1166,9 +1421,7 @@
|
||||
});
|
||||
|
||||
const textData = canvas.toDataURL('image/png');
|
||||
state.images = [];
|
||||
loadImage(textData, 'text');
|
||||
closePanel();
|
||||
showPreviewResult(textData, '文字图片');
|
||||
}
|
||||
|
||||
function applyResize() {
|
||||
@@ -1184,9 +1437,7 @@
|
||||
ctx.drawImage(img.img, 0, 0, newWidth, newHeight);
|
||||
|
||||
const resizedData = canvas.toDataURL('image/png');
|
||||
state.images = [];
|
||||
loadImage(resizedData, 'resized');
|
||||
closePanel();
|
||||
showPreviewResult(resizedData, '调整大小');
|
||||
}
|
||||
|
||||
function applyCrop() {
|
||||
@@ -1204,9 +1455,7 @@
|
||||
ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h);
|
||||
|
||||
const croppedData = canvas.toDataURL('image/png');
|
||||
state.images = [];
|
||||
loadImage(croppedData, 'cropped');
|
||||
closePanel();
|
||||
showPreviewResult(croppedData, '裁剪');
|
||||
}
|
||||
|
||||
function saveImage() {
|
||||
@@ -1245,7 +1494,6 @@
|
||||
document.getElementById('historyList').innerHTML = '<span class="text-gray-400 text-xs">暂无操作</span>';
|
||||
showEmptyHint();
|
||||
document.getElementById('splitResults').classList.add('hidden');
|
||||
closePanel();
|
||||
}
|
||||
|
||||
// 监听合并面板渲染
|
||||
|
||||
Reference in New Issue
Block a user