Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bd5b3354 |
12
app.py
12
app.py
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
图片编辑器 - Image Editor v1.2.7
|
图片编辑器 - Image Editor v1.2.3
|
||||||
前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等
|
前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等
|
||||||
v1.2.7: 修复合并图片紧密拼接问题
|
v1.2.3: 执行操作后保持当前配置面板
|
||||||
|
|
||||||
端口: 19018
|
端口: 19018
|
||||||
"""
|
"""
|
||||||
@@ -25,7 +25,7 @@ def index():
|
|||||||
|
|
||||||
@app.route('/api/health')
|
@app.route('/api/health')
|
||||||
def health():
|
def health():
|
||||||
return jsonify({'status': 'ok', 'version': '1.2.7', 'time': datetime.now().isoformat()})
|
return jsonify({'status': 'ok', 'version': '1.2.3', 'time': datetime.now().isoformat()})
|
||||||
|
|
||||||
@app.route('/api/save', methods=['POST'])
|
@app.route('/api/save', methods=['POST'])
|
||||||
def save_image():
|
def save_image():
|
||||||
@@ -64,9 +64,9 @@ def list_images():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("图片编辑器 - Image Editor v1.2.7")
|
print("图片编辑器 - Image Editor v1.2.4")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("修复合并图片紧密拼接问题")
|
print("拖放图片上传,选中效果优化")
|
||||||
print(f"访问地址: http://localhost:19018")
|
print(f"访问地址: http://localhost:19018")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
app.run(host='0.0.0.0', port=19018, debug=True)
|
app.run(host='0.0.0.0', port=19018, debug=True)rt=19018, debug=True)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 763 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 763 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 768 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 767 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 796 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>图片编辑器 - Image Editor v1.2.7</title>
|
<title>图片编辑器 - Image Editor v1.2.4</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 功能按钮 -->
|
<!-- 功能按钮 -->
|
||||||
<div class="mt-3 flex gap-2 flex-wrap">
|
<div class="mt-3 flex gap-2 flex-wrap">
|
||||||
<button onclick="setTool('merge')" id="tool-merge" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
|
<button onclick="setTool('merge')" id="tool-merge" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
|
||||||
@@ -128,13 +128,13 @@
|
|||||||
<p class="text-sm mt-1">支持多张图片同时上传</p>
|
<p class="text-sm mt-1">支持多张图片同时上传</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片列表 -->
|
<!-- 图片列表 -->
|
||||||
<div id="imageList" class="mt-4 hidden">
|
<div id="imageList" class="mt-4 hidden">
|
||||||
<h3 class="text-sm font-medium text-gray-600 mb-2">已加载图片 <span id="imageCount">(0张)</span></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 id="imageItems" class="flex gap-2 overflow-x-auto pb-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分割结果展示 -->
|
<!-- 分割结果展示 -->
|
||||||
<div id="splitResults" class="mt-4 hidden">
|
<div id="splitResults" 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">分割结果</h3>
|
||||||
@@ -149,10 +149,10 @@
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 id="panelTitle" class="font-bold text-gray-800">操作面板</h2>
|
<h2 id="panelTitle" class="font-bold text-gray-800">操作面板</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 配置内容区域 -->
|
<!-- 配置内容区域 -->
|
||||||
<div id="panelContent"></div>
|
<div id="panelContent"></div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div id="panelActions" class="mt-4 pt-4 border-t"></div>
|
<div id="panelActions" class="mt-4 pt-4 border-t"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,143 +183,18 @@
|
|||||||
selection: null,
|
selection: null,
|
||||||
textStyles: { bold: false, italic: false },
|
textStyles: { bold: false, italic: false },
|
||||||
mergeImageOrder: [],
|
mergeImageOrder: [],
|
||||||
mergeImageSizes: {},
|
mergeImageSizes: {}
|
||||||
previewMode: false, // 预览模式
|
|
||||||
previewData: null, // 预览图片数据
|
|
||||||
originalImages: null // 预览前保存的原始图片
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const canvas = document.getElementById('mainCanvas');
|
const canvas = document.getElementById('mainCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const container = document.getElementById('canvasContainer');
|
const container = document.getElementById('canvasContainer');
|
||||||
|
|
||||||
// 检查是否有图片(画布有内容)
|
// 返回首页面板
|
||||||
function hasImage() {
|
function goHome() {
|
||||||
return state.images.length > 0 || document.getElementById('emptyHint').classList.contains('hidden');
|
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
}
|
state.currentTool = null;
|
||||||
// 显示预览效果(操作后显示结果,但保留原始图片)
|
showPanel('home');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新当前工具面板(执行操作后保持当前配置)
|
// 刷新当前工具面板(执行操作后保持当前配置)
|
||||||
@@ -359,13 +234,13 @@
|
|||||||
function setTool(tool) {
|
function setTool(tool) {
|
||||||
// 移除所有工具的 active 状态
|
// 移除所有工具的 active 状态
|
||||||
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
// 设置当前工具 active
|
// 设置当前工具 active
|
||||||
const btn = document.getElementById('tool-' + tool);
|
const btn = document.getElementById('tool-' + tool);
|
||||||
if (btn) btn.classList.add('active');
|
if (btn) btn.classList.add('active');
|
||||||
|
|
||||||
state.currentTool = tool;
|
state.currentTool = tool;
|
||||||
|
|
||||||
// 更新右侧配置面板内容(不隐藏)
|
// 更新右侧配置面板内容(不隐藏)
|
||||||
showPanel(tool);
|
showPanel(tool);
|
||||||
}
|
}
|
||||||
@@ -375,7 +250,7 @@
|
|||||||
const panel = document.getElementById('rightPanel');
|
const panel = document.getElementById('rightPanel');
|
||||||
// 确保面板始终显示
|
// 确保面板始终显示
|
||||||
panel.classList.remove('hidden-panel');
|
panel.classList.remove('hidden-panel');
|
||||||
|
|
||||||
const configs = getPanelConfig(tool);
|
const configs = getPanelConfig(tool);
|
||||||
document.getElementById('panelTitle').textContent = configs.title;
|
document.getElementById('panelTitle').textContent = configs.title;
|
||||||
document.getElementById('panelContent').innerHTML = configs.content;
|
document.getElementById('panelContent').innerHTML = configs.content;
|
||||||
@@ -434,18 +309,18 @@
|
|||||||
actions: ''
|
actions: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(tool) {
|
switch(tool) {
|
||||||
|
|
||||||
case 'merge':
|
case 'merge':
|
||||||
if (!hasImage() || state.images.length < 2) {
|
if (state.images.length < 2) {
|
||||||
return {
|
return {
|
||||||
title: '合并图片',
|
title: '合并图片',
|
||||||
content: `<p class="text-sm text-red-500">请先上传至少2张图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传至少2张图片</p>`,
|
||||||
actions: `<button onclick="goHome()" 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>`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
state.mergeImageOrder = state.images.map((img, idx) => ({
|
state.mergeImageOrder = state.images.map((img, idx) => ({
|
||||||
index: idx,
|
index: idx,
|
||||||
name: img.name,
|
name: img.name,
|
||||||
@@ -457,7 +332,7 @@
|
|||||||
state.mergeImageOrder.forEach(item => {
|
state.mergeImageOrder.forEach(item => {
|
||||||
state.mergeImageSizes[item.index] = { width: item.width, height: item.height };
|
state.mergeImageSizes[item.index] = { width: item.width, height: item.height };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '合并图片',
|
title: '合并图片',
|
||||||
content: `
|
content: `
|
||||||
@@ -513,9 +388,9 @@
|
|||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'split':
|
case 'split':
|
||||||
if (!hasImage()) {
|
if (state.images.length === 0) {
|
||||||
return {
|
return {
|
||||||
title: '分割图片',
|
title: '分割图片',
|
||||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||||
@@ -544,9 +419,9 @@
|
|||||||
`,
|
`,
|
||||||
actions: `<button onclick="applySplit()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行分割</button>`
|
actions: `<button onclick="applySplit()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行分割</button>`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'hole':
|
case 'hole':
|
||||||
if (!hasImage()) {
|
if (state.images.length === 0) {
|
||||||
return {
|
return {
|
||||||
title: '挖孔填充',
|
title: '挖孔填充',
|
||||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||||
@@ -590,9 +465,9 @@
|
|||||||
<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg w-full">关闭面板</button>
|
<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg w-full">关闭面板</button>
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'circle':
|
case 'circle':
|
||||||
if (!hasImage()) {
|
if (state.images.length === 0) {
|
||||||
return {
|
return {
|
||||||
title: '圆形切图',
|
title: '圆形切图',
|
||||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||||
@@ -629,7 +504,7 @@
|
|||||||
`,
|
`,
|
||||||
actions: `<button onclick="applyCircleCut()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行切图</button>`
|
actions: `<button onclick="applyCircleCut()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行切图</button>`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
return {
|
return {
|
||||||
title: '文字图片',
|
title: '文字图片',
|
||||||
@@ -676,9 +551,9 @@
|
|||||||
`,
|
`,
|
||||||
actions: `<button onclick="createTextImage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">生成图片</button>`
|
actions: `<button onclick="createTextImage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">生成图片</button>`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'resize':
|
case 'resize':
|
||||||
if (!hasImage()) {
|
if (state.images.length === 0) {
|
||||||
return {
|
return {
|
||||||
title: '调整大小',
|
title: '调整大小',
|
||||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||||
@@ -707,9 +582,9 @@
|
|||||||
`,
|
`,
|
||||||
actions: `<button onclick="applyResize()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用</button>`
|
actions: `<button onclick="applyResize()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用</button>`
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'crop':
|
case 'crop':
|
||||||
if (!hasImage()) {
|
if (state.images.length === 0) {
|
||||||
return {
|
return {
|
||||||
title: '裁剪',
|
title: '裁剪',
|
||||||
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
|
||||||
@@ -746,7 +621,7 @@
|
|||||||
<button onclick="applyCrop()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用裁剪</button>
|
<button onclick="applyCrop()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用裁剪</button>
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { title: '配置', content: '', actions: '' };
|
return { title: '配置', content: '', actions: '' };
|
||||||
}
|
}
|
||||||
@@ -756,29 +631,29 @@
|
|||||||
function initMergePanelContent() {
|
function initMergePanelContent() {
|
||||||
const orderContainer = document.getElementById('mergeImageOrderPanel');
|
const orderContainer = document.getElementById('mergeImageOrderPanel');
|
||||||
if (!orderContainer) return;
|
if (!orderContainer) return;
|
||||||
|
|
||||||
orderContainer.innerHTML = '';
|
orderContainer.innerHTML = '';
|
||||||
state.mergeImageOrder.forEach((item, idx) => {
|
state.mergeImageOrder.forEach((item, idx) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'image-order-item';
|
div.className = 'image-order-item';
|
||||||
div.draggable = true;
|
div.draggable = true;
|
||||||
div.dataset.index = idx;
|
div.dataset.index = idx;
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<span class="drag-handle"><i class="ri-draggable"></i></span>
|
<span class="drag-handle"><i class="ri-draggable"></i></span>
|
||||||
<img src="${item.dataUrl}" class="w-10 h-10 object-cover rounded">
|
<img src="${item.dataUrl}" class="w-10 h-10 object-cover rounded">
|
||||||
<span class="text-xs flex-1 truncate">${item.name}</span>
|
<span class="text-xs flex-1 truncate">${item.name}</span>
|
||||||
<span class="text-xs bg-blue-500 text-white rounded px-1">#${idx + 1}</span>
|
<span class="text-xs bg-blue-500 text-white rounded px-1">#${idx + 1}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
div.addEventListener('dragstart', handleDragStart);
|
div.addEventListener('dragstart', handleDragStart);
|
||||||
div.addEventListener('dragover', handleDragOver);
|
div.addEventListener('dragover', handleDragOver);
|
||||||
div.addEventListener('drop', handleDrop);
|
div.addEventListener('drop', handleDrop);
|
||||||
div.addEventListener('dragend', handleDragEnd);
|
div.addEventListener('dragend', handleDragEnd);
|
||||||
|
|
||||||
orderContainer.appendChild(div);
|
orderContainer.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderMergeSizeList();
|
renderMergeSizeList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +672,7 @@
|
|||||||
const checkbox = document.getElementById('mergeUniformSize');
|
const checkbox = document.getElementById('mergeUniformSize');
|
||||||
const uniformSettings = document.getElementById('uniformSizeSettings');
|
const uniformSettings = document.getElementById('uniformSizeSettings');
|
||||||
const sizeList = document.getElementById('mergeSizeList');
|
const sizeList = document.getElementById('mergeSizeList');
|
||||||
|
|
||||||
if (checkbox && checkbox.checked) {
|
if (checkbox && checkbox.checked) {
|
||||||
if (uniformSettings) uniformSettings.classList.remove('hidden');
|
if (uniformSettings) uniformSettings.classList.remove('hidden');
|
||||||
if (sizeList) sizeList.classList.add('hidden');
|
if (sizeList) sizeList.classList.add('hidden');
|
||||||
@@ -812,20 +687,20 @@
|
|||||||
function renderMergeSizeList() {
|
function renderMergeSizeList() {
|
||||||
const container = document.getElementById('mergeSizeList');
|
const container = document.getElementById('mergeSizeList');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
state.mergeImageOrder.forEach((item, idx) => {
|
state.mergeImageOrder.forEach((item, idx) => {
|
||||||
const originalIndex = item.index;
|
const originalIndex = item.index;
|
||||||
const size = state.mergeImageSizes[originalIndex] || { width: item.width, height: item.height };
|
const size = state.mergeImageSizes[originalIndex] || { width: item.width, height: item.height };
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex items-center gap-1 text-xs py-1';
|
div.className = 'flex items-center gap-1 text-xs py-1';
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<span class="truncate w-20">${item.name}</span>
|
<span class="truncate w-20">${item.name}</span>
|
||||||
<input type="number" value="${size.width}"
|
<input type="number" value="${size.width}"
|
||||||
onchange="state.mergeImageSizes[${originalIndex}].width = parseInt(this.value) || ${item.width}"
|
onchange="state.mergeImageSizes[${originalIndex}].width = parseInt(this.value) || ${item.width}"
|
||||||
class="border rounded px-1 py-0.5 w-14" placeholder="宽">
|
class="border rounded px-1 py-0.5 w-14" placeholder="宽">
|
||||||
<input type="number" value="${size.height}"
|
<input type="number" value="${size.height}"
|
||||||
onchange="state.mergeImageSizes[${originalIndex}].height = parseInt(this.value) || ${item.height}"
|
onchange="state.mergeImageSizes[${originalIndex}].height = parseInt(this.value) || ${item.height}"
|
||||||
class="border rounded px-1 py-0.5 w-14" placeholder="高">
|
class="border rounded px-1 py-0.5 w-14" placeholder="高">
|
||||||
`;
|
`;
|
||||||
@@ -850,11 +725,11 @@
|
|||||||
if (draggedItem !== this) {
|
if (draggedItem !== this) {
|
||||||
const fromIdx = parseInt(draggedItem.dataset.index);
|
const fromIdx = parseInt(draggedItem.dataset.index);
|
||||||
const toIdx = parseInt(this.dataset.index);
|
const toIdx = parseInt(this.dataset.index);
|
||||||
|
|
||||||
const temp = state.mergeImageOrder[fromIdx];
|
const temp = state.mergeImageOrder[fromIdx];
|
||||||
state.mergeImageOrder[fromIdx] = state.mergeImageOrder[toIdx];
|
state.mergeImageOrder[fromIdx] = state.mergeImageOrder[toIdx];
|
||||||
state.mergeImageOrder[toIdx] = temp;
|
state.mergeImageOrder[toIdx] = temp;
|
||||||
|
|
||||||
initMergePanelContent();
|
initMergePanelContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -979,17 +854,10 @@
|
|||||||
updateImageList();
|
updateImageList();
|
||||||
hideEmptyHint();
|
hideEmptyHint();
|
||||||
saveState('加载: ' + name);
|
saveState('加载: ' + name);
|
||||||
// 刷新当前面板
|
|
||||||
refreshCurrentPanel();
|
|
||||||
};
|
};
|
||||||
img.src = dataUrl;
|
img.src = dataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载操作结果(不清空原始图片)
|
|
||||||
function loadOperationResult(dataUrl, name, operationName) {
|
|
||||||
showPreviewResult(dataUrl, operationName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCanvas() {
|
function drawCanvas() {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
@@ -1010,7 +878,7 @@
|
|||||||
const list = document.getElementById('imageList');
|
const list = document.getElementById('imageList');
|
||||||
const items = document.getElementById('imageItems');
|
const items = document.getElementById('imageItems');
|
||||||
document.getElementById('imageCount').textContent = `(${state.images.length}张)`;
|
document.getElementById('imageCount').textContent = `(${state.images.length}张)`;
|
||||||
if (!hasImage()) { list.classList.add('hidden'); return; }
|
if (state.images.length === 0) { list.classList.add('hidden'); return; }
|
||||||
list.classList.remove('hidden');
|
list.classList.remove('hidden');
|
||||||
items.innerHTML = '';
|
items.innerHTML = '';
|
||||||
state.images.forEach((item, index) => {
|
state.images.forEach((item, index) => {
|
||||||
@@ -1030,7 +898,7 @@
|
|||||||
drawCanvas();
|
drawCanvas();
|
||||||
updateImageList();
|
updateImageList();
|
||||||
saveState('移除图片');
|
saveState('移除图片');
|
||||||
if (!hasImage()) showEmptyHint();
|
if (state.images.length === 0) showEmptyHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideEmptyHint() {
|
function hideEmptyHint() {
|
||||||
@@ -1056,21 +924,21 @@
|
|||||||
|
|
||||||
function applyMerge() {
|
function applyMerge() {
|
||||||
if (!document.getElementById('mergeDirection')) return;
|
if (!document.getElementById('mergeDirection')) return;
|
||||||
|
|
||||||
const direction = document.getElementById('mergeDirection').value;
|
const direction = document.getElementById('mergeDirection').value;
|
||||||
const gap = parseInt(document.getElementById('mergeGap')?.value) || 0;
|
const gap = parseInt(document.getElementById('mergeGap')?.value) || 0;
|
||||||
const bgColor = document.getElementById('mergeBgColor')?.value || '#ffffff';
|
const bgColor = document.getElementById('mergeBgColor')?.value || '#ffffff';
|
||||||
const uniformSize = document.getElementById('mergeUniformSize')?.checked;
|
const uniformSize = document.getElementById('mergeUniformSize')?.checked;
|
||||||
const uniformWidth = parseInt(document.getElementById('mergeUniformWidth')?.value) || 0;
|
const uniformWidth = parseInt(document.getElementById('mergeUniformWidth')?.value) || 0;
|
||||||
const uniformHeight = parseInt(document.getElementById('mergeUniformHeight')?.value) || 0;
|
const uniformHeight = parseInt(document.getElementById('mergeUniformHeight')?.value) || 0;
|
||||||
|
|
||||||
let cols = 1, rows = state.mergeImageOrder.length;
|
let cols = 1, rows = state.mergeImageOrder.length;
|
||||||
if (direction === 'horizontal') { cols = state.mergeImageOrder.length; rows = 1; }
|
if (direction === 'horizontal') { cols = state.mergeImageOrder.length; rows = 1; }
|
||||||
else if (direction === 'grid') {
|
else if (direction === 'grid') {
|
||||||
cols = parseInt(document.getElementById('gridCols')?.value) || 2;
|
cols = parseInt(document.getElementById('gridCols')?.value) || 2;
|
||||||
rows = parseInt(document.getElementById('gridRows')?.value) || 2;
|
rows = parseInt(document.getElementById('gridRows')?.value) || 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActualSize = (item) => {
|
const getActualSize = (item) => {
|
||||||
if (uniformSize) {
|
if (uniformSize) {
|
||||||
const maxWidth = Math.max(...state.mergeImageOrder.map(i => i.width));
|
const maxWidth = Math.max(...state.mergeImageOrder.map(i => i.width));
|
||||||
@@ -1080,136 +948,53 @@
|
|||||||
const size = state.mergeImageSizes[item.index] || { width: item.width, height: item.height };
|
const size = state.mergeImageSizes[item.index] || { width: item.width, height: item.height };
|
||||||
return { width: size.width || item.width, height: size.height || item.height };
|
return { width: size.width || item.width, height: size.height || item.height };
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算画布尺寸
|
let maxWidth = 0, maxHeight = 0;
|
||||||
let canvasWidth = 0, canvasHeight = 0;
|
state.mergeImageOrder.forEach(item => {
|
||||||
|
const size = getActualSize(item);
|
||||||
if (uniformSize) {
|
maxWidth = Math.max(maxWidth, size.width);
|
||||||
// 统一尺寸:使用固定单元格大小
|
maxHeight = Math.max(maxHeight, size.height);
|
||||||
let maxWidth = 0, maxHeight = 0;
|
});
|
||||||
state.mergeImageOrder.forEach(item => {
|
|
||||||
const size = getActualSize(item);
|
canvas.width = cols * maxWidth + (cols - 1) * gap;
|
||||||
maxWidth = Math.max(maxWidth, size.width);
|
canvas.height = rows * maxHeight + (rows - 1) * gap;
|
||||||
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 = canvasWidth;
|
|
||||||
canvas.height = canvasHeight;
|
|
||||||
|
|
||||||
// 绘制合并结果到画布(预览)
|
|
||||||
ctx.fillStyle = bgColor;
|
ctx.fillStyle = bgColor;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// 绘制图片
|
state.mergeImageOrder.forEach((orderItem, index) => {
|
||||||
let currentX = 0, currentY = 0;
|
if (index >= cols * rows) return;
|
||||||
|
const col = index % cols;
|
||||||
if (uniformSize) {
|
const row = Math.floor(index / cols);
|
||||||
// 统一尺寸:固定单元格,居中绘制
|
const x = col * (maxWidth + gap);
|
||||||
let maxWidth = 0, maxHeight = 0;
|
const y = row * (maxHeight + gap);
|
||||||
state.mergeImageOrder.forEach(item => {
|
const size = getActualSize(orderItem);
|
||||||
const size = getActualSize(item);
|
const offsetX = (maxWidth - size.width) / 2;
|
||||||
maxWidth = Math.max(maxWidth, size.width);
|
const offsetY = (maxHeight - size.height) / 2;
|
||||||
maxHeight = Math.max(maxHeight, size.height);
|
|
||||||
});
|
const originalImg = state.images.find(img => img.name === orderItem.name);
|
||||||
|
if (originalImg) {
|
||||||
state.mergeImageOrder.forEach((orderItem, index) => {
|
ctx.drawImage(originalImg.img, x + offsetX, y + offsetY, size.width, size.height);
|
||||||
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 size = getActualSize(orderItem);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} 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');
|
const mergedData = canvas.toDataURL('image/png');
|
||||||
showPreviewResult(mergedData, '合并图片');
|
state.images = [];
|
||||||
|
loadImage(mergedData, 'merged');
|
||||||
|
refreshCurrentPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySplit() {
|
function applySplit() {
|
||||||
const cols = parseInt(document.getElementById('splitCols')?.value) || 2;
|
const cols = parseInt(document.getElementById('splitCols')?.value) || 2;
|
||||||
const rows = parseInt(document.getElementById('splitRows')?.value) || 2;
|
const rows = parseInt(document.getElementById('splitRows')?.value) || 2;
|
||||||
|
|
||||||
const img = state.images[0];
|
const img = state.images[0];
|
||||||
const pieceWidth = img.width / cols;
|
const pieceWidth = img.width / cols;
|
||||||
const pieceHeight = img.height / rows;
|
const pieceHeight = img.height / rows;
|
||||||
|
|
||||||
const resultsDiv = document.getElementById('splitImages');
|
const resultsDiv = document.getElementById('splitImages');
|
||||||
resultsDiv.innerHTML = '';
|
resultsDiv.innerHTML = '';
|
||||||
|
|
||||||
for (let r = 0; r < rows; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const pieceCanvas = document.createElement('canvas');
|
const pieceCanvas = document.createElement('canvas');
|
||||||
@@ -1217,13 +1002,13 @@
|
|||||||
pieceCanvas.height = pieceHeight;
|
pieceCanvas.height = pieceHeight;
|
||||||
const pieceCtx = pieceCanvas.getContext('2d');
|
const pieceCtx = pieceCanvas.getContext('2d');
|
||||||
pieceCtx.drawImage(img.img, c * pieceWidth, r * pieceHeight, pieceWidth, pieceHeight, 0, 0, pieceWidth, pieceHeight);
|
pieceCtx.drawImage(img.img, c * pieceWidth, r * pieceHeight, pieceWidth, pieceHeight, 0, 0, pieceWidth, pieceHeight);
|
||||||
|
|
||||||
const dataUrl = pieceCanvas.toDataURL('image/png');
|
const dataUrl = pieceCanvas.toDataURL('image/png');
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'relative';
|
div.className = 'relative';
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<img src="${dataUrl}" class="w-full rounded border">
|
<img src="${dataUrl}" class="w-full rounded border">
|
||||||
<button onclick="downloadDataUrl('${dataUrl}', 'split_${r}_${c}.png')"
|
<button onclick="downloadDataUrl('${dataUrl}', 'split_${r}_${c}.png')"
|
||||||
class="absolute bottom-1 right-1 bg-blue-500 text-white px-1 py-0.5 rounded text-xs">
|
class="absolute bottom-1 right-1 bg-blue-500 text-white px-1 py-0.5 rounded text-xs">
|
||||||
<i class="ri-download-line"></i>
|
<i class="ri-download-line"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -1231,7 +1016,7 @@
|
|||||||
resultsDiv.appendChild(div);
|
resultsDiv.appendChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('splitResults').classList.remove('hidden');
|
document.getElementById('splitResults').classList.remove('hidden');
|
||||||
refreshCurrentPanel();
|
refreshCurrentPanel();
|
||||||
saveState(`分割 (${cols}x${rows})`);
|
saveState(`分割 (${cols}x${rows})`);
|
||||||
@@ -1268,7 +1053,7 @@
|
|||||||
const mouseUp = () => {
|
const mouseUp = () => {
|
||||||
if (!state.isSelecting) return;
|
if (!state.isSelecting) return;
|
||||||
state.isSelecting = false;
|
state.isSelecting = false;
|
||||||
|
|
||||||
if (state.currentTool === 'hole') {
|
if (state.currentTool === 'hole') {
|
||||||
applyHole();
|
applyHole();
|
||||||
} else if (state.currentTool === 'crop') {
|
} else if (state.currentTool === 'crop') {
|
||||||
@@ -1277,7 +1062,7 @@
|
|||||||
document.getElementById('cropWidth').value = Math.round(Math.abs(state.selection.width));
|
document.getElementById('cropWidth').value = Math.round(Math.abs(state.selection.width));
|
||||||
document.getElementById('cropHeight').value = Math.round(Math.abs(state.selection.height));
|
document.getElementById('cropHeight').value = Math.round(Math.abs(state.selection.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
state.selection = null;
|
state.selection = null;
|
||||||
state.currentTool = null;
|
state.currentTool = null;
|
||||||
drawCanvas();
|
drawCanvas();
|
||||||
@@ -1292,18 +1077,18 @@
|
|||||||
|
|
||||||
function applyHole() {
|
function applyHole() {
|
||||||
if (!document.getElementById('holeShape')) return;
|
if (!document.getElementById('holeShape')) return;
|
||||||
|
|
||||||
const shape = document.getElementById('holeShape').value;
|
const shape = document.getElementById('holeShape').value;
|
||||||
const fill = document.getElementById('holeFill').value;
|
const fill = document.getElementById('holeFill').value;
|
||||||
const color = document.getElementById('holeColor')?.value || '#ffffff';
|
const color = document.getElementById('holeColor')?.value || '#ffffff';
|
||||||
const blurRadius = parseInt(document.getElementById('holeBlurRadius')?.value) || 10;
|
const blurRadius = parseInt(document.getElementById('holeBlurRadius')?.value) || 10;
|
||||||
|
|
||||||
const sel = state.selection;
|
const sel = state.selection;
|
||||||
const x = sel.width < 0 ? sel.x + sel.width : sel.x;
|
const x = sel.width < 0 ? sel.x + sel.width : sel.x;
|
||||||
const y = sel.height < 0 ? sel.y + sel.height : sel.y;
|
const y = sel.height < 0 ? sel.y + sel.height : sel.y;
|
||||||
const w = Math.abs(sel.width);
|
const w = Math.abs(sel.width);
|
||||||
const h = Math.abs(sel.height);
|
const h = Math.abs(sel.height);
|
||||||
|
|
||||||
if (fill === 'color') {
|
if (fill === 'color') {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
if (shape === 'rectangle') ctx.fillRect(x, y, w, h);
|
if (shape === 'rectangle') ctx.fillRect(x, y, w, h);
|
||||||
@@ -1330,23 +1115,23 @@
|
|||||||
|
|
||||||
function applyCircleCut() {
|
function applyCircleCut() {
|
||||||
if (!document.getElementById('circleEdge')) return;
|
if (!document.getElementById('circleEdge')) return;
|
||||||
|
|
||||||
const diameter = document.getElementById('circleDiameter')?.value;
|
const diameter = document.getElementById('circleDiameter')?.value;
|
||||||
const edge = document.getElementById('circleEdge').value;
|
const edge = document.getElementById('circleEdge').value;
|
||||||
const borderColor = document.getElementById('circleBorderColor')?.value || '#3b82f6';
|
const borderColor = document.getElementById('circleBorderColor')?.value || '#3b82f6';
|
||||||
const borderWidth = parseInt(document.getElementById('circleBorderWidth')?.value) || 3;
|
const borderWidth = parseInt(document.getElementById('circleBorderWidth')?.value) || 3;
|
||||||
|
|
||||||
const img = state.images[0];
|
const img = state.images[0];
|
||||||
const size = diameter ? parseInt(diameter) : Math.min(img.width, img.height);
|
const size = diameter ? parseInt(diameter) : Math.min(img.width, img.height);
|
||||||
|
|
||||||
const circleCanvas = document.createElement('canvas');
|
const circleCanvas = document.createElement('canvas');
|
||||||
circleCanvas.width = size; circleCanvas.height = size;
|
circleCanvas.width = size; circleCanvas.height = size;
|
||||||
const circleCtx = circleCanvas.getContext('2d');
|
const circleCtx = circleCanvas.getContext('2d');
|
||||||
|
|
||||||
circleCtx.beginPath();
|
circleCtx.beginPath();
|
||||||
circleCtx.arc(size/2, size/2, size/2 - (edge === 'border' ? borderWidth : 0), 0, Math.PI * 2);
|
circleCtx.arc(size/2, size/2, size/2 - (edge === 'border' ? borderWidth : 0), 0, Math.PI * 2);
|
||||||
circleCtx.closePath();
|
circleCtx.closePath();
|
||||||
|
|
||||||
if (edge === 'smooth') {
|
if (edge === 'smooth') {
|
||||||
const gradient = circleCtx.createRadialGradient(size/2, size/2, size/2 - 10, size/2, size/2, size/2);
|
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)');
|
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
||||||
@@ -1354,12 +1139,12 @@
|
|||||||
circleCtx.fillStyle = gradient;
|
circleCtx.fillStyle = gradient;
|
||||||
circleCtx.fill();
|
circleCtx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
circleCtx.clip();
|
circleCtx.clip();
|
||||||
const offsetX = (img.width - size) / 2;
|
const offsetX = (img.width - size) / 2;
|
||||||
const offsetY = (img.height - size) / 2;
|
const offsetY = (img.height - size) / 2;
|
||||||
circleCtx.drawImage(img.img, -offsetX, -offsetY);
|
circleCtx.drawImage(img.img, -offsetX, -offsetY);
|
||||||
|
|
||||||
if (edge === 'border') {
|
if (edge === 'border') {
|
||||||
circleCtx.strokeStyle = borderColor;
|
circleCtx.strokeStyle = borderColor;
|
||||||
circleCtx.lineWidth = borderWidth;
|
circleCtx.lineWidth = borderWidth;
|
||||||
@@ -1367,95 +1152,103 @@
|
|||||||
circleCtx.arc(size/2, size/2, size/2 - borderWidth/2, 0, Math.PI * 2);
|
circleCtx.arc(size/2, size/2, size/2 - borderWidth/2, 0, Math.PI * 2);
|
||||||
circleCtx.stroke();
|
circleCtx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.width = size; canvas.height = size;
|
canvas.width = size; canvas.height = size;
|
||||||
ctx.drawImage(circleCanvas, 0, 0);
|
ctx.drawImage(circleCanvas, 0, 0);
|
||||||
|
|
||||||
const circleData = canvas.toDataURL('image/png');
|
const circleData = canvas.toDataURL('image/png');
|
||||||
showPreviewResult(circleData, '圆形切图');
|
state.images = [];
|
||||||
|
loadImage(circleData, 'circle');
|
||||||
|
refreshCurrentPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTextImage() {
|
function createTextImage() {
|
||||||
if (!document.getElementById('textContent')) return;
|
if (!document.getElementById('textContent')) return;
|
||||||
|
|
||||||
const content = document.getElementById('textContent').value;
|
const content = document.getElementById('textContent').value;
|
||||||
if (!content.trim()) { alert('请输入文字'); return; }
|
if (!content.trim()) { alert('请输入文字'); return; }
|
||||||
|
|
||||||
const font = document.getElementById('textFont').value;
|
const font = document.getElementById('textFont').value;
|
||||||
const size = parseInt(document.getElementById('textSize')?.value) || 48;
|
const size = parseInt(document.getElementById('textSize')?.value) || 48;
|
||||||
const color = document.getElementById('textColor')?.value || '#000000';
|
const color = document.getElementById('textColor')?.value || '#000000';
|
||||||
const transparentBg = document.getElementById('textTransparentBg')?.checked;
|
const transparentBg = document.getElementById('textTransparentBg')?.checked;
|
||||||
const bgColor = document.getElementById('textBgColor')?.value || '#ffffff';
|
const bgColor = document.getElementById('textBgColor')?.value || '#ffffff';
|
||||||
|
|
||||||
const fontStyle = (state.textStyles.bold ? 'bold ' : '') + (state.textStyles.italic ? 'italic ' : '') + size + 'px ' + font;
|
const fontStyle = (state.textStyles.bold ? 'bold ' : '') + (state.textStyles.italic ? 'italic ' : '') + size + 'px ' + font;
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCtx.font = fontStyle;
|
tempCtx.font = fontStyle;
|
||||||
|
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
lines.forEach(line => maxWidth = Math.max(maxWidth, tempCtx.measureText(line).width));
|
lines.forEach(line => maxWidth = Math.max(maxWidth, tempCtx.measureText(line).width));
|
||||||
|
|
||||||
const padding = 20;
|
const padding = 20;
|
||||||
const lineHeight = size * 1.2;
|
const lineHeight = size * 1.2;
|
||||||
const totalHeight = lines.length * lineHeight + padding * 2;
|
const totalHeight = lines.length * lineHeight + padding * 2;
|
||||||
const totalWidth = maxWidth + padding * 2;
|
const totalWidth = maxWidth + padding * 2;
|
||||||
|
|
||||||
canvas.width = totalWidth; canvas.height = totalHeight;
|
canvas.width = totalWidth; canvas.height = totalHeight;
|
||||||
|
|
||||||
if (!transparentBg) {
|
if (!transparentBg) {
|
||||||
ctx.fillStyle = bgColor;
|
ctx.fillStyle = bgColor;
|
||||||
ctx.fillRect(0, 0, totalWidth, totalHeight);
|
ctx.fillRect(0, 0, totalWidth, totalHeight);
|
||||||
} else {
|
} else {
|
||||||
ctx.clearRect(0, 0, totalWidth, totalHeight);
|
ctx.clearRect(0, 0, totalWidth, totalHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.font = fontStyle;
|
ctx.font = fontStyle;
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
ctx.fillText(line, totalWidth/2, padding + index * lineHeight);
|
ctx.fillText(line, totalWidth/2, padding + index * lineHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
const textData = canvas.toDataURL('image/png');
|
const textData = canvas.toDataURL('image/png');
|
||||||
showPreviewResult(textData, '文字图片');
|
state.images = [];
|
||||||
|
loadImage(textData, 'text');
|
||||||
|
refreshCurrentPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyResize() {
|
function applyResize() {
|
||||||
if (!document.getElementById('newWidth')) return;
|
if (!document.getElementById('newWidth')) return;
|
||||||
|
|
||||||
const newWidth = parseInt(document.getElementById('newWidth').value);
|
const newWidth = parseInt(document.getElementById('newWidth').value);
|
||||||
const newHeight = parseInt(document.getElementById('newHeight').value);
|
const newHeight = parseInt(document.getElementById('newHeight').value);
|
||||||
|
|
||||||
if (!newWidth || !newHeight) { alert('请输入有效尺寸'); return; }
|
if (!newWidth || !newHeight) { alert('请输入有效尺寸'); return; }
|
||||||
|
|
||||||
const img = state.images[0];
|
const img = state.images[0];
|
||||||
canvas.width = newWidth; canvas.height = newHeight;
|
canvas.width = newWidth; canvas.height = newHeight;
|
||||||
ctx.drawImage(img.img, 0, 0, newWidth, newHeight);
|
ctx.drawImage(img.img, 0, 0, newWidth, newHeight);
|
||||||
|
|
||||||
const resizedData = canvas.toDataURL('image/png');
|
const resizedData = canvas.toDataURL('image/png');
|
||||||
showPreviewResult(resizedData, '调整大小');
|
state.images = [];
|
||||||
|
loadImage(resizedData, 'resized');
|
||||||
|
refreshCurrentPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCrop() {
|
function applyCrop() {
|
||||||
if (!document.getElementById('cropWidth')) return;
|
if (!document.getElementById('cropWidth')) return;
|
||||||
|
|
||||||
const x = parseInt(document.getElementById('cropX').value) || 0;
|
const x = parseInt(document.getElementById('cropX').value) || 0;
|
||||||
const y = parseInt(document.getElementById('cropY').value) || 0;
|
const y = parseInt(document.getElementById('cropY').value) || 0;
|
||||||
const w = parseInt(document.getElementById('cropWidth').value);
|
const w = parseInt(document.getElementById('cropWidth').value);
|
||||||
const h = parseInt(document.getElementById('cropHeight').value);
|
const h = parseInt(document.getElementById('cropHeight').value);
|
||||||
|
|
||||||
if (!w || !h) { alert('请输入有效尺寸'); return; }
|
if (!w || !h) { alert('请输入有效尺寸'); return; }
|
||||||
|
|
||||||
const img = state.images[0];
|
const img = state.images[0];
|
||||||
canvas.width = w; canvas.height = h;
|
canvas.width = w; canvas.height = h;
|
||||||
ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h);
|
ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h);
|
||||||
|
|
||||||
const croppedData = canvas.toDataURL('image/png');
|
const croppedData = canvas.toDataURL('image/png');
|
||||||
showPreviewResult(croppedData, '裁剪');
|
state.images = [];
|
||||||
|
loadImage(croppedData, 'cropped');
|
||||||
|
refreshCurrentPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveImage() {
|
function saveImage() {
|
||||||
|
|||||||
Reference in New Issue
Block a user