Files
image-editor/templates/index.html

1512 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
.canvas-container {
position: relative;
background: #f0f0f0;
border: 2px dashed #ccc;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.canvas-container.has-image {
background: transparent;
border: none;
}
.tool-btn {
transition: all 0.2s;
}
.tool-btn:hover {
transform: scale(1.05);
}
.tool-btn.active {
background: #3b82f6 !important;
color: white !important;
}
.right-panel {
transition: all 0.3s;
overflow-y: auto;
}
.right-panel.hidden-panel {
width: 0;
padding: 0;
overflow: hidden;
}
.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">
<!-- 顶部工具栏 -->
<div class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-full mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-800">
<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>
<button onclick="downloadImage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
<i class="ri-download-line mr-1"></i>下载
</button>
<button onclick="clearCanvas()" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600">
<i class="ri-delete-bin-line mr-1"></i>清空
</button>
</div>
</div>
<!-- 功能按钮 -->
<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">
<i class="ri-stack-line mr-1"></i>合并图片
</button>
<button onclick="setTool('split')" id="tool-split" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-layout-grid-line mr-1"></i>分割图片
</button>
<button onclick="setTool('hole')" id="tool-hole" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-contrast-drop-line mr-1"></i>挖孔填充
</button>
<button onclick="setTool('circle')" id="tool-circle" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-checkbox-blank-circle-line mr-1"></i>圆形切图
</button>
<button onclick="setTool('text')" id="tool-text" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-font-size mr-1"></i>文字图片
</button>
<button onclick="setTool('resize')" id="tool-resize" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-expand-diagonal-line mr-1"></i>调整大小
</button>
<button onclick="setTool('crop')" id="tool-crop" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-crop-line mr-1"></i>裁剪
</button>
</div>
</div>
</div>
<!-- 主内容区域(左右布局) -->
<div class="flex h-[calc(100vh-120px)]">
<!-- 左侧画布区域 -->
<div class="flex-1 p-4 overflow-auto">
<div class="bg-white rounded-lg shadow-sm p-4">
<div id="canvasContainer" class="canvas-container rounded-lg overflow-hidden">
<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="text-sm mt-1">支持多张图片同时上传</p>
</div>
</div>
<!-- 图片列表 -->
<div id="imageList" class="mt-4 hidden">
<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 id="splitResults" class="mt-4 hidden">
<h3 class="text-sm font-medium text-gray-600 mb-2">分割结果</h3>
<div id="splitImages" class="grid grid-cols-4 gap-4"></div>
</div>
</div>
</div>
<!-- 右侧配置面板 - 始终显示 -->
<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>
</div>
<!-- 配置内容区域 -->
<div id="panelContent"></div>
<!-- 操作按钮 -->
<div id="panelActions" class="mt-4 pt-4 border-t"></div>
</div>
</div>
</div>
<!-- 操作历史(底部) -->
<div class="fixed bottom-0 left-0 right-0 bg-white shadow-sm p-2">
<div class="max-w-full mx-auto px-4">
<h3 class="text-xs font-medium text-gray-600 mb-1">操作历史 <span id="historyInfo">(当前: 0/0)</span></h3>
<div id="historyList" class="flex gap-2 overflow-x-auto">
<span class="text-gray-400 text-xs">暂无操作</span>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input type="file" id="fileInput" accept="image/*" multiple onchange="handleFileSelect(event)">
<script>
// 全局状态
const state = {
images: [],
historyStack: [],
redoStack: [],
currentTool: null,
isSelecting: false,
selection: null,
textStyles: { bold: false, italic: false },
mergeImageOrder: [],
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() {
canvas.width = 800;
canvas.height = 600;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 设置拖放
function setupDragDrop() {
container.addEventListener('dragover', (e) => {
e.preventDefault();
});
container.addEventListener('drop', (e) => {
e.preventDefault();
handleFiles(e.dataTransfer.files);
});
}
// 设置当前工具
function setTool(tool) {
// 移除所有工具的 active 状态
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
// 设置当前工具 active
const btn = document.getElementById('tool-' + tool);
if (btn) btn.classList.add('active');
state.currentTool = tool;
// 更新右侧配置面板内容(不隐藏)
showPanel(tool);
}
// 显示右侧面板内容
function showPanel(tool) {
const panel = document.getElementById('rightPanel');
// 确保面板始终显示
panel.classList.remove('hidden-panel');
const configs = getPanelConfig(tool);
document.getElementById('panelTitle').textContent = configs.title;
document.getElementById('panelContent').innerHTML = configs.content;
document.getElementById('panelActions').innerHTML = configs.actions;
}
// 获取各工具的配置面板内容
function getPanelConfig(tool) {
<!-- 默认首页面板 -->
if (!tool || tool === 'home') {
return {
title: '欢迎使用',
content: `
<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 (!hasImage() || state.images.length < 2) {
return {
title: '合并图片',
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>`
};
}
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 };
});
return {
title: '合并图片',
content: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">图片顺序(拖动调整)</label>
<div id="mergeImageOrderPanel" class="border rounded-lg p-2 min-h-100 max-h-200 overflow-y-auto"></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">
<option value="horizontal">横向合并(左右拼接)</option>
<option value="vertical">纵向合并(上下拼接)</option>
<option value="grid">网格合并</option>
</select>
</div>
<div id="gridSettings" class="hidden">
<div class="flex gap-2">
<input type="number" id="gridCols" value="2" min="1" max="10" class="border rounded-lg px-3 py-2 w-1/2" placeholder="列数">
<input type="number" id="gridRows" value="2" min="1" max="10" class="border rounded-lg px-3 py-2 w-1/2" placeholder="行数">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">间距(像素)</label>
<input type="number" id="mergeGap" value="0" min="0" max="100" class="w-full border rounded-lg px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">背景颜色</label>
<input type="color" id="mergeBgColor" value="#ffffff" class="w-full h-10 rounded-lg">
</div>
<div class="border-t pt-3">
<div class="flex items-center gap-2 mb-2">
<input type="checkbox" id="mergeUniformSize" onchange="toggleUniformSize()" class="rounded">
<span class="text-sm">统一尺寸</span>
</div>
<div id="uniformSizeSettings" class="hidden 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 id="mergeSizeList"></div>
</div>
</div>
`,
actions: `
<button onclick="applyMerge()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">
<i class="ri-stack-line mr-1"></i>执行合并
</button>
`
};
case 'split':
if (!hasImage()) {
return {
title: '分割图片',
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
};
}
return {
title: '分割图片',
content: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">分割数量</label>
<div class="flex gap-2">
<div class="w-1/2">
<label class="block text-xs text-gray-500">列数</label>
<input type="number" id="splitCols" value="2" min="1" max="20" class="w-full border rounded-lg px-3 py-2">
</div>
<div class="w-1/2">
<label class="block text-xs text-gray-500">行数</label>
<input type="number" id="splitRows" value="2" min="1" max="20" class="w-full border rounded-lg px-3 py-2">
</div>
</div>
</div>
<p class="text-xs text-gray-500">分割后的图片将显示在画布下方,可单独下载</p>
</div>
`,
actions: `<button onclick="applySplit()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行分割</button>`
};
case 'hole':
if (!hasImage()) {
return {
title: '挖孔填充',
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
};
}
return {
title: '挖孔填充',
content: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">挖孔形状</label>
<select id="holeShape" class="w-full border rounded-lg px-3 py-2">
<option value="rectangle">矩形</option>
<option value="circle">圆形</option>
<option value="ellipse">椭圆</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">填充方式</label>
<select id="holeFill" onchange="toggleHoleFillSettings()" class="w-full border rounded-lg px-3 py-2">
<option value="color">填充颜色</option>
<option value="transparent">透明</option>
<option value="blur">模糊</option>
<option value="content-aware">智能填充</option>
</select>
</div>
<div id="holeColorPicker">
<label class="block text-sm font-medium text-gray-600 mb-1">填充颜色</label>
<input type="color" id="holeColor" value="#ffffff" class="w-full h-10 rounded-lg">
</div>
<div id="holeBlurSetting" class="hidden">
<label class="block text-sm font-medium text-gray-600 mb-1">模糊程度</label>
<input type="range" id="holeBlurRadius" value="10" min="1" max="50" class="w-full">
</div>
<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 mb-2">开始选择</button>
<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg w-full">关闭面板</button>
`
};
case 'circle':
if (!hasImage()) {
return {
title: '圆形切图',
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
};
}
return {
title: '圆形切图',
content: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">圆形直径</label>
<input type="number" id="circleDiameter" placeholder="留空则自动" class="w-full border rounded-lg px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">边缘处理</label>
<select id="circleEdge" onchange="toggleCircleBorder()" class="w-full border rounded-lg px-3 py-2">
<option value="sharp">锐利边缘</option>
<option value="smooth">平滑边缘</option>
<option value="border">带边框</option>
</select>
</div>
<div id="circleBorderSetting" class="hidden space-y-2">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">边框颜色</label>
<input type="color" id="circleBorderColor" value="#3b82f6" class="w-full h-10 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">边框宽度</label>
<input type="number" id="circleBorderWidth" value="3" min="1" max="20" class="w-full border rounded-lg px-3 py-2">
</div>
</div>
</div>
`,
actions: `<button onclick="applyCircleCut()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">执行切图</button>`
};
case 'text':
return {
title: '文字图片',
content: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">文字内容</label>
<textarea id="textContent" rows="3" class="w-full border rounded-lg px-3 py-2" placeholder="输入文字..."></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">字体</label>
<select id="textFont" class="w-full border rounded-lg px-3 py-2">
<option value="Arial">Arial</option>
<option value="Microsoft YaHei">微软雅黑</option>
<option value="SimSun">宋体</option>
<option value="SimHei">黑体</option>
<option value="KaiTi">楷体</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">字体大小</label>
<input type="number" id="textSize" value="48" min="12" max="200" class="w-full border rounded-lg px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">文字颜色</label>
<input type="color" id="textColor" value="#000000" class="w-full h-10 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">背景</label>
<div class="flex items-center gap-2 mb-1">
<input type="checkbox" id="textTransparentBg" class="rounded">
<span class="text-xs">透明背景</span>
</div>
<input type="color" id="textBgColor" value="#ffffff" class="w-full h-10 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">样式</label>
<div class="flex gap-2">
<button id="textBold" onclick="toggleTextStyle('bold')" class="px-3 py-1 border rounded text-sm font-bold">B</button>
<button id="textItalic" onclick="toggleTextStyle('italic')" class="px-3 py-1 border rounded text-sm italic">I</button>
</div>
</div>
</div>
`,
actions: `<button onclick="createTextImage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">生成图片</button>`
};
case 'resize':
if (!hasImage()) {
return {
title: '调整大小',
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
};
}
const img = state.images[0];
return {
title: '调整大小',
content: `
<div class="space-y-4">
<p class="text-xs text-gray-500">当前尺寸: ${img.width} × ${img.height}</p>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">新宽度</label>
<input type="number" id="newWidth" value="${img.width}" class="w-full border rounded-lg px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">新高度</label>
<input type="number" id="newHeight" value="${img.height}" class="w-full border rounded-lg px-3 py-2">
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="keepRatio" checked class="rounded">
<span class="text-xs">保持宽高比例</span>
</div>
</div>
`,
actions: `<button onclick="applyResize()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用</button>`
};
case 'crop':
if (!hasImage()) {
return {
title: '裁剪',
content: `<p class="text-sm text-red-500">请先上传图片</p>`,
actions: `<button onclick="goHome()" class="px-4 py-2 bg-gray-200 rounded-lg">关闭</button>`
};
}
return {
title: '裁剪',
content: `
<div class="space-y-4">
<p class="text-xs text-gray-500">输入裁剪坐标,或点击"开始选择"在画布上拖动</p>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs text-gray-500">起始 X</label>
<input type="number" id="cropX" value="0" class="w-full border rounded-lg px-2 py-1">
</div>
<div>
<label class="block text-xs text-gray-500">起始 Y</label>
<input type="number" id="cropY" value="0" class="w-full border rounded-lg px-2 py-1">
</div>
<div>
<label class="block text-xs text-gray-500">宽度</label>
<input type="number" id="cropWidth" class="w-full border rounded-lg px-2 py-1">
</div>
<div>
<label class="block text-xs text-gray-500">高度</label>
<input type="number" id="cropHeight" class="w-full border rounded-lg px-2 py-1">
</div>
</div>
</div>
`,
actions: `
<button onclick="startCropSelection()" class="px-4 py-2 bg-gray-200 rounded-lg w-full mb-2">开始选择</button>
<button onclick="applyCrop()" class="px-4 py-2 bg-blue-500 text-white rounded-lg w-full">应用裁剪</button>
`
};
default:
return { title: '配置', content: '', actions: '' };
}
}
// 初始化合并面板内容
function initMergePanelContent() {
const orderContainer = document.getElementById('mergeImageOrderPanel');
if (!orderContainer) return;
orderContainer.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-10 h-10 object-cover rounded">
<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>
`;
div.addEventListener('dragstart', handleDragStart);
div.addEventListener('dragover', handleDragOver);
div.addEventListener('drop', handleDrop);
div.addEventListener('dragend', handleDragEnd);
orderContainer.appendChild(div);
});
renderMergeSizeList();
}
// 监听合并方式变化
document.addEventListener('change', (e) => {
if (e.target.id === 'mergeDirection') {
const gridSettings = document.getElementById('gridSettings');
if (gridSettings) {
gridSettings.classList.toggle('hidden', e.target.value !== 'grid');
}
}
});
// 切换统一尺寸
function toggleUniformSize() {
const checkbox = document.getElementById('mergeUniformSize');
const uniformSettings = document.getElementById('uniformSizeSettings');
const sizeList = document.getElementById('mergeSizeList');
if (checkbox && checkbox.checked) {
if (uniformSettings) uniformSettings.classList.remove('hidden');
if (sizeList) sizeList.classList.add('hidden');
} else {
if (uniformSettings) uniformSettings.classList.add('hidden');
if (sizeList) sizeList.classList.remove('hidden');
renderMergeSizeList();
}
}
// 渲染单独尺寸设置列表
function renderMergeSizeList() {
const container = document.getElementById('mergeSizeList');
if (!container) return;
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-1 text-xs py-1';
div.innerHTML = `
<span class="truncate w-20">${item.name}</span>
<input type="number" value="${size.width}"
onchange="state.mergeImageSizes[${originalIndex}].width = parseInt(this.value) || ${item.width}"
class="border rounded px-1 py-0.5 w-14" placeholder="宽">
<input type="number" value="${size.height}"
onchange="state.mergeImageSizes[${originalIndex}].height = parseInt(this.value) || ${item.height}"
class="border rounded px-1 py-0.5 w-14" placeholder="高">
`;
container.appendChild(div);
});
}
// 拖拽处理
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;
initMergePanelContent();
}
}
function handleDragEnd(e) {
this.style.opacity = '1';
draggedItem = null;
}
// 挖孔填充设置切换
function toggleHoleFillSettings() {
const fill = document.getElementById('holeFill').value;
document.getElementById('holeColorPicker').classList.toggle('hidden', fill !== 'color');
document.getElementById('holeBlurSetting').classList.toggle('hidden', fill !== 'blur');
}
// 圆形边框设置切换
function toggleCircleBorder() {
const edge = document.getElementById('circleEdge').value;
document.getElementById('circleBorderSetting').classList.toggle('hidden', edge !== 'border');
}
// 文字样式切换
function toggleTextStyle(style) {
state.textStyles[style] = !state.textStyles[style];
const btn = document.getElementById('text' + style.charAt(0).toUpperCase() + style.slice(1));
btn.classList.toggle('bg-blue-500');
btn.classList.toggle('text-white');
}
// 保存状态
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
});
state.redoStack = [];
updateUndoRedoButtons();
addHistory(actionName);
}
function undo() {
if (state.historyStack.length === 0) return;
state.redoStack.push({
imageData: canvas.toDataURL('image/png'),
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
});
restoreState(state.historyStack.pop());
updateUndoRedoButtons();
}
function redo() {
if (state.redoStack.length === 0) return;
state.historyStack.push({
imageData: canvas.toDataURL('image/png'),
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: '恢复'
});
restoreState(state.redoStack.pop());
updateUndoRedoButtons();
}
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 { ...item, img: i };
});
updateImageList();
state.images.length > 0 ? hideEmptyHint() : 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 handleFileSelect(event) { handleFiles(event.target.files); }
function handleFiles(files) {
for (let file of files) {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => loadImage(e.target.result, file.name);
reader.readAsDataURL(file);
}
}
}
function loadImage(dataUrl, name) {
const img = new Image();
img.onload = () => {
state.images.push({ img, name, x: 0, y: 0, width: img.width, height: img.height, dataUrl });
if (state.images.length === 1) {
canvas.width = img.width;
canvas.height = img.height;
}
drawCanvas();
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';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let item of state.images) {
ctx.drawImage(item.img, item.x, item.y, item.width, item.height);
}
if (state.selection) {
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.strokeRect(state.selection.x, state.selection.y, state.selection.width, state.selection.height);
ctx.setLineDash([]);
}
}
function updateImageList() {
const list = document.getElementById('imageList');
const items = document.getElementById('imageItems');
document.getElementById('imageCount').textContent = `(${state.images.length}张)`;
if (!hasImage()) { list.classList.add('hidden'); return; }
list.classList.remove('hidden');
items.innerHTML = '';
state.images.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'flex items-center gap-2 bg-gray-100 rounded-lg px-3 py-2';
div.innerHTML = `
<span class="text-sm truncate">${item.name}</span>
<span class="text-xs text-gray-500">${item.width}×${item.height}</span>
<button onclick="removeImage(${index})" class="text-red-500"><i class="ri-close-line"></i></button>
`;
items.appendChild(div);
});
}
function removeImage(index) {
state.images.splice(index, 1);
drawCanvas();
updateImageList();
saveState('移除图片');
if (!hasImage()) showEmptyHint();
}
function hideEmptyHint() {
document.getElementById('emptyHint').classList.add('hidden');
container.classList.add('has-image');
}
function showEmptyHint() {
document.getElementById('emptyHint').classList.remove('hidden');
container.classList.remove('has-image');
}
function addHistory(action) {
const list = document.getElementById('historyList');
if (list.querySelector('.text-gray-400')) list.innerHTML = '';
const span = document.createElement('span');
span.className = 'text-xs bg-blue-100 text-blue-700 rounded px-2 py-1 whitespace-nowrap';
span.textContent = action;
list.appendChild(span);
}
// === 执行各操作 ===
function applyMerge() {
if (!document.getElementById('mergeDirection')) return;
const direction = document.getElementById('mergeDirection').value;
const gap = parseInt(document.getElementById('mergeGap')?.value) || 0;
const bgColor = document.getElementById('mergeBgColor')?.value || '#ffffff';
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.mergeImageOrder.length;
if (direction === 'horizontal') { 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) {
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 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 = 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;
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');
showPreviewResult(mergedData, '合并图片');
}
function applySplit() {
const cols = parseInt(document.getElementById('splitCols')?.value) || 2;
const rows = parseInt(document.getElementById('splitRows')?.value) || 2;
const img = state.images[0];
const pieceWidth = img.width / cols;
const pieceHeight = img.height / rows;
const resultsDiv = document.getElementById('splitImages');
resultsDiv.innerHTML = '';
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const pieceCanvas = document.createElement('canvas');
pieceCanvas.width = pieceWidth;
pieceCanvas.height = pieceHeight;
const pieceCtx = pieceCanvas.getContext('2d');
pieceCtx.drawImage(img.img, c * pieceWidth, r * pieceHeight, pieceWidth, pieceHeight, 0, 0, pieceWidth, pieceHeight);
const dataUrl = pieceCanvas.toDataURL('image/png');
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${dataUrl}" class="w-full rounded border">
<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">
<i class="ri-download-line"></i>
</button>
`;
resultsDiv.appendChild(div);
}
}
document.getElementById('splitResults').classList.remove('hidden');
refreshCurrentPanel();
saveState(`分割 (${cols}x${rows})`);
}
function startHoleSelection() {
state.isSelecting = true;
state.currentTool = 'hole';
setupSelectionListeners();
}
function startCropSelection() {
state.isSelecting = true;
state.currentTool = 'crop';
setupSelectionListeners();
}
function setupSelectionListeners() {
let startX, startY;
const mouseDown = (e) => {
if (!state.isSelecting) return;
const rect = canvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
state.selection = { x: startX, y: startY, width: 0, height: 0 };
};
const mouseMove = (e) => {
if (!state.isSelecting || !state.selection) return;
const rect = canvas.getBoundingClientRect();
state.selection.width = e.clientX - rect.left - startX;
state.selection.height = e.clientY - rect.top - startY;
drawCanvas();
};
const mouseUp = () => {
if (!state.isSelecting) return;
state.isSelecting = false;
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));
document.getElementById('cropHeight').value = Math.round(Math.abs(state.selection.height));
}
state.selection = null;
state.currentTool = null;
drawCanvas();
canvas.removeEventListener('mousedown', mouseDown);
canvas.removeEventListener('mousemove', mouseMove);
canvas.removeEventListener('mouseup', mouseUp);
};
canvas.addEventListener('mousedown', mouseDown);
canvas.addEventListener('mousemove', mouseMove);
canvas.addEventListener('mouseup', mouseUp);
}
function applyHole() {
if (!document.getElementById('holeShape')) return;
const shape = document.getElementById('holeShape').value;
const fill = document.getElementById('holeFill').value;
const color = document.getElementById('holeColor')?.value || '#ffffff';
const blurRadius = parseInt(document.getElementById('holeBlurRadius')?.value) || 10;
const sel = state.selection;
const x = sel.width < 0 ? sel.x + sel.width : sel.x;
const y = sel.height < 0 ? sel.y + sel.height : sel.y;
const w = Math.abs(sel.width);
const h = Math.abs(sel.height);
if (fill === 'color') {
ctx.fillStyle = color;
if (shape === 'rectangle') ctx.fillRect(x, y, w, h);
else if (shape === 'circle') { ctx.beginPath(); ctx.arc(x + w/2, y + h/2, Math.min(w, h)/2, 0, Math.PI * 2); ctx.fill(); }
else { ctx.beginPath(); ctx.ellipse(x + w/2, y + h/2, w/2, h/2, 0, 0, Math.PI * 2); ctx.fill(); }
} 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 pixel = ctx.getImageData(Math.max(0, x - 1), Math.max(0, y - 1), 1, 1).data;
ctx.fillStyle = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;
if (shape === 'rectangle') ctx.fillRect(x, y, w, h);
else { ctx.beginPath(); ctx.arc(x + w/2, y + h/2, Math.min(w, h)/2, 0, Math.PI * 2); ctx.fill(); }
}
saveState('挖孔填充');
}
function applyCircleCut() {
if (!document.getElementById('circleEdge')) return;
const diameter = document.getElementById('circleDiameter')?.value;
const edge = document.getElementById('circleEdge').value;
const borderColor = document.getElementById('circleBorderColor')?.value || '#3b82f6';
const borderWidth = parseInt(document.getElementById('circleBorderWidth')?.value) || 3;
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') {
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(1, 'rgba(255,255,255,0)');
circleCtx.fillStyle = gradient;
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;
circleCtx.beginPath();
circleCtx.arc(size/2, size/2, size/2 - borderWidth/2, 0, Math.PI * 2);
circleCtx.stroke();
}
canvas.width = size; canvas.height = size;
ctx.drawImage(circleCanvas, 0, 0);
const circleData = canvas.toDataURL('image/png');
showPreviewResult(circleData, '圆形切图');
}
function createTextImage() {
if (!document.getElementById('textContent')) return;
const content = document.getElementById('textContent').value;
if (!content.trim()) { alert('请输入文字'); return; }
const font = document.getElementById('textFont').value;
const size = parseInt(document.getElementById('textSize')?.value) || 48;
const color = document.getElementById('textColor')?.value || '#000000';
const transparentBg = document.getElementById('textTransparentBg')?.checked;
const bgColor = document.getElementById('textBgColor')?.value || '#ffffff';
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;
const lines = content.split('\n');
let maxWidth = 0;
lines.forEach(line => maxWidth = Math.max(maxWidth, tempCtx.measureText(line).width));
const padding = 20;
const lineHeight = size * 1.2;
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);
} else {
ctx.clearRect(0, 0, totalWidth, totalHeight);
}
ctx.font = fontStyle;
ctx.fillStyle = color;
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
lines.forEach((line, index) => {
ctx.fillText(line, totalWidth/2, padding + index * lineHeight);
});
const textData = canvas.toDataURL('image/png');
showPreviewResult(textData, '文字图片');
}
function applyResize() {
if (!document.getElementById('newWidth')) return;
const newWidth = parseInt(document.getElementById('newWidth').value);
const newHeight = parseInt(document.getElementById('newHeight').value);
if (!newWidth || !newHeight) { alert('请输入有效尺寸'); return; }
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');
showPreviewResult(resizedData, '调整大小');
}
function applyCrop() {
if (!document.getElementById('cropWidth')) return;
const x = parseInt(document.getElementById('cropX').value) || 0;
const y = parseInt(document.getElementById('cropY').value) || 0;
const w = parseInt(document.getElementById('cropWidth').value);
const h = parseInt(document.getElementById('cropHeight').value);
if (!w || !h) { alert('请输入有效尺寸'); return; }
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');
showPreviewResult(croppedData, '裁剪');
}
function saveImage() {
const dataUrl = canvas.toDataURL('image/png');
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: dataUrl, filename: 'edited_' + Date.now() + '.png' })
}).then(res => res.json()).then(data => {
if (data.success) {
alert('保存成功: ' + data.filename);
saveState('保存');
}
});
}
function downloadImage() {
downloadDataUrl(canvas.toDataURL('image/png'), 'edited_' + Date.now() + '.png');
saveState('下载');
}
function downloadDataUrl(dataUrl, filename) {
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
link.click();
}
function clearCanvas() {
state.images = [];
state.historyStack = [];
state.redoStack = [];
setupCanvas();
updateImageList();
updateUndoRedoButtons();
document.getElementById('historyList').innerHTML = '<span class="text-gray-400 text-xs">暂无操作</span>';
showEmptyHint();
document.getElementById('splitResults').classList.add('hidden');
}
// 监听合并面板渲染
document.addEventListener('DOMContentLoaded', () => {
init();
// 监听 panelContent 变化
const observer = new MutationObserver(() => {
if (state.currentTool === 'merge' && state.images.length >= 2) {
initMergePanelContent();
}
});
observer.observe(document.getElementById('panelContent'), { childList: true });
});
</script>
</body>
</html>