Files
image-editor/templates/index.html

1194 lines
50 KiB
HTML

<!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</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;
color: white;
}
.image-item {
cursor: move;
position: absolute;
}
.selection-box {
border: 2px solid #3b82f6;
background: rgba(59, 130, 246, 0.1);
position: absolute;
pointer-events: none;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 10px;
max-width: 500px;
width: 90%;
}
input[type="file"] {
display: none;
}
.drop-zone {
border: 3px dashed #3b82f6;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.drop-zone:hover {
background: rgba(59, 130, 246, 0.1);
}
</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-6xl 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="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="uploadImage()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-upload-cloud-line mr-1"></i>上传图片
</button>
<button onclick="showMergeModal()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-stack-line mr-1"></i>合并图片
</button>
<button onclick="showSplitModal()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-layout-grid-line mr-1"></i>分割图片
</button>
<button onclick="showHoleModal()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-contrast-drop-line mr-1"></i>挖孔填充
</button>
<button onclick="showCircleModal()" 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="showTextModal()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-font-size mr-1"></i>文字图片
</button>
<button onclick="showResizeModal()" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-expand-diagonal-line mr-1"></i>调整大小
</button>
<button onclick="showCropModal()" 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="max-w-6xl mx-auto px-4 py-6">
<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="dropOverlay" class="drop-zone absolute inset-0 hidden">
<i class="ri-upload-cloud-2-line text-6xl text-blue-500"></i>
<p class="mt-2 text-gray-600">拖放图片到此处</p>
</div>
<div id="emptyHint" class="text-gray-400 text-center">
<i class="ri-image-add-line text-6xl"></i>
<p class="mt-2">点击"上传图片"或拖放图片开始编辑</p>
</div>
</div>
<!-- 图片列表 -->
<div id="imageList" class="mt-4 hidden">
<h3 class="text-sm font-medium text-gray-600 mb-2">已加载图片</h3>
<div id="imageItems" class="flex gap-2 overflow-x-auto pb-2"></div>
</div>
</div>
<!-- 操作历史 -->
<div class="bg-white rounded-lg shadow-sm p-4 mt-4">
<h3 class="text-sm font-medium text-gray-600 mb-2">操作历史</h3>
<div id="historyList" class="flex gap-2 overflow-x-auto pb-2">
<span class="text-gray-400 text-sm">暂无操作</span>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input type="file" id="fileInput" accept="image/*" multiple onchange="handleFileSelect(event)">
<!-- 合并图片模态框 -->
<div id="mergeModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-stack-line mr-2"></i>合并图片</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-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">
<label class="block text-sm font-medium text-gray-600 mb-1">网格设置</label>
<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>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('mergeModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="applyMerge()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">应用</button>
</div>
</div>
</div>
<!-- 分割图片模态框 -->
<div id="splitModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-layout-grid-line mr-2"></i>分割图片</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">分割方式</label>
<select id="splitMode" class="w-full border rounded-lg px-3 py-2">
<option value="grid">等分网格</option>
<option value="custom">自定义分割</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">分割数量</label>
<div class="flex gap-2">
<input type="number" id="splitCols" value="2" min="1" max="20" class="border rounded-lg px-3 py-2 w-1/2" placeholder="列数">
<input type="number" id="splitRows" value="2" min="1" max="20" class="border rounded-lg px-3 py-2 w-1/2" placeholder="行数">
</div>
</div>
<p class="text-sm text-gray-500">分割后的图片将显示在画布下方,可单独下载</p>
</div>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('splitModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="applySplit()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">应用</button>
</div>
</div>
</div>
<!-- 挖孔填充模态框 -->
<div id="holeModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-contrast-drop-line mr-2"></i>挖孔填充</h2>
<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" 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-sm text-gray-500">在画布上拖动选择要挖孔的区域</p>
</div>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('holeModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="startHoleSelection()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">开始选择</button>
</div>
</div>
</div>
<!-- 圆形切图模态框 -->
<div id="circleModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-checkbox-blank-circle-line mr-2"></i>圆形切图</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">圆形直径</label>
<input type="number" id="circleDiameter" value="200" min="10" class="w-full border rounded-lg px-3 py-2">
<p class="text-sm text-gray-500 mt-1">留空则在画布上拖动选择区域</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">边缘处理</label>
<select id="circleEdge" 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">
<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">
<label class="block text-sm font-medium text-gray-600 mb-1 mt-2">边框宽度</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 class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('circleModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="applyCircleCut()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">应用</button>
</div>
</div>
</div>
<!-- 文字图片模态框 -->
<div id="textModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-font-size mr-2"></i>文字图片</h2>
<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>
<option value="sans-serif">Sans-serif</option>
<option value="serif">Serif</option>
<option value="monospace">Monospace</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">
<input type="checkbox" id="textTransparentBg" class="rounded">
<span class="text-sm">透明背景</span>
</div>
<input type="color" id="textBgColor" value="#ffffff" class="w-full h-10 rounded-lg mt-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">文字样式</label>
<div class="flex gap-2">
<button id="textBold" onclick="toggleStyle('bold')" class="px-3 py-1 border rounded">B</button>
<button id="textItalic" onclick="toggleStyle('italic')" class="px-3 py-1 border rounded italic">I</button>
<select id="textAlign" class="border rounded px-2 py-1">
<option value="left">左对齐</option>
<option value="center">居中</option>
<option value="right">右对齐</option>
</select>
</div>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('textModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="createTextImage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">生成</button>
</div>
</div>
</div>
<!-- 调整大小模态框 -->
<div id="resizeModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-expand-diagonal-line mr-2"></i>调整大小</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">新宽度</label>
<input type="number" id="newWidth" 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" 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-sm">保持宽高比例</span>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('resizeModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="applyResize()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">应用</button>
</div>
</div>
</div>
<!-- 裁剪模态框 -->
<div id="cropModal" class="modal">
<div class="modal-content">
<h2 class="text-lg font-bold mb-4"><i class="ri-crop-line mr-2"></i>裁剪</h2>
<p class="text-sm text-gray-500 mb-4">在画布上拖动选择裁剪区域,或输入具体坐标</p>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">起始 X</label>
<input type="number" id="cropX" value="0" class="w-full border rounded-lg px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">起始 Y</label>
<input type="number" id="cropY" value="0" 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="cropWidth" 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="cropHeight" class="w-full border rounded-lg px-3 py-2">
</div>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button onclick="closeModal('cropModal')" class="px-4 py-2 bg-gray-200 rounded-lg">取消</button>
<button onclick="startCropSelection()" class="px-4 py-2 bg-blue-500 text-white rounded-lg">开始选择</button>
<button onclick="applyCrop()" class="px-4 py-2 bg-green-500 text-white rounded-lg">应用裁剪</button>
</div>
</div>
</div>
<!-- 分割结果展示 -->
<div id="splitResults" class="hidden max-w-6xl mx-auto px-4 pb-6">
<div class="bg-white rounded-lg shadow-sm p-4">
<h3 class="text-sm font-medium text-gray-600 mb-4">分割结果</h3>
<div id="splitImages" class="grid grid-cols-4 gap-4"></div>
</div>
</div>
<script>
// 全局状态
const state = {
images: [],
history: [],
currentTool: null,
isSelecting: false,
selection: null,
textStyles: { bold: false, italic: false }
};
// Canvas 相关
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('canvasContainer');
// 初始化
function init() {
setupDragDrop();
setupCanvas();
}
// 设置画布
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();
document.getElementById('dropOverlay').classList.remove('hidden');
});
container.addEventListener('dragleave', () => {
document.getElementById('dropOverlay').classList.add('hidden');
});
container.addEventListener('drop', (e) => {
e.preventDefault();
document.getElementById('dropOverlay').classList.add('hidden');
handleFiles(e.dataTransfer.files);
});
}
// 上传图片
function uploadImage() {
document.getElementById('fileInput').click();
}
// 处理文件选择
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: img,
name: name,
x: 0,
y: 0,
width: img.width,
height: img.height
});
// 调整画布大小以适应图片
if (state.images.length === 1) {
canvas.width = img.width;
canvas.height = img.height;
}
drawCanvas();
updateImageList();
hideEmptyHint();
addHistory('加载图片: ' + name);
};
img.src = dataUrl;
}
// 绘制画布
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');
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 cursor-pointer hover:bg-gray-200';
div.innerHTML = `
<span class="text-sm truncate max-w-150">${item.name}</span>
<button onclick="removeImage(${index})" class="text-red-500 hover:text-red-600">
<i class="ri-close-line"></i>
</button>
`;
items.appendChild(div);
});
}
// 移除图片
function removeImage(index) {
state.images.splice(index, 1);
drawCanvas();
updateImageList();
addHistory('移除图片');
if (state.images.length === 0) {
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) {
state.history.push({
action: action,
time: new Date().toLocaleTimeString()
});
updateHistoryList();
}
// 更新历史列表
function updateHistoryList() {
const list = document.getElementById('historyList');
list.innerHTML = '';
state.history.slice(-10).reverse().forEach(item => {
const span = document.createElement('span');
span.className = 'text-xs bg-gray-100 rounded px-2 py-1';
span.textContent = item.action + ' (' + item.time + ')';
list.appendChild(span);
});
}
// 显示模态框
function showModal(id) {
document.getElementById(id).classList.add('show');
}
// 关闭模态框
function closeModal(id) {
document.getElementById(id).classList.remove('show');
}
// === 功能实现 ===
// 合并图片
function showMergeModal() {
if (state.images.length < 2) {
alert('请先上传至少2张图片');
return;
}
showModal('mergeModal');
}
function applyMerge() {
const direction = document.getElementById('mergeDirection').value;
const gap = parseInt(document.getElementById('mergeGap').value) || 0;
const bgColor = document.getElementById('mergeBgColor').value;
let cols = 1, rows = state.images.length;
if (direction === 'horizontal') {
cols = state.images.length;
rows = 1;
} else if (direction === 'grid') {
cols = parseInt(document.getElementById('gridCols').value) || 2;
rows = parseInt(document.getElementById('gridRows').value) || 2;
}
// 计算合并后的尺寸
let maxWidth = 0, maxHeight = 0;
state.images.forEach(item => {
maxWidth = Math.max(maxWidth, item.width);
maxHeight = Math.max(maxHeight, item.height);
});
const totalWidth = cols * maxWidth + (cols - 1) * gap;
const totalHeight = rows * maxHeight + (rows - 1) * gap;
// 创建新画布
canvas.width = totalWidth;
canvas.height = totalHeight;
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, totalWidth, totalHeight);
// 绘制图片
state.images.forEach((item, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * (maxWidth + gap);
const y = row * (maxHeight + gap);
// 居中绘制
const offsetX = (maxWidth - item.width) / 2;
const offsetY = (maxHeight - item.height) / 2;
ctx.drawImage(item.img, x + offsetX, y + offsetY, item.width, item.height);
});
// 清空图片列表(合并后为单张)
const mergedData = canvas.toDataURL('image/png');
state.images = [];
loadImage(mergedData, 'merged_' + new Date().getTime());
closeModal('mergeModal');
addHistory('合并图片 (' + direction + ')');
}
// 监听合并方式变化
document.getElementById('mergeDirection').addEventListener('change', (e) => {
document.getElementById('gridSettings').classList.toggle('hidden', e.target.value !== 'grid');
});
// 分割图片
function showSplitModal() {
if (state.images.length === 0) {
alert('请先上传图片');
return;
}
showModal('splitModal');
// 设置默认尺寸
const img = state.images[0];
document.getElementById('cropWidth').value = img.width;
document.getElementById('cropHeight').value = img.height;
}
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="downloadSplitPiece('${dataUrl}', 'split_${r}_${c}.png')"
class="absolute bottom-2 right-2 bg-blue-500 text-white px-2 py-1 rounded text-xs">
<i class="ri-download-line"></i>
</button>
`;
resultsDiv.appendChild(div);
}
}
document.getElementById('splitResults').classList.remove('hidden');
closeModal('splitModal');
addHistory(`分割图片 (${cols}x${rows})`);
}
// 下载分割片段
function downloadSplitPiece(dataUrl, filename) {
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
link.click();
}
// 挖孔填充
function showHoleModal() {
if (state.images.length === 0) {
alert('请先上传图片');
return;
}
showModal('holeModal');
}
// 监听填充方式变化
document.getElementById('holeFill').addEventListener('change', (e) => {
const colorPicker = document.getElementById('holeColorPicker');
const blurSetting = document.getElementById('holeBlurSetting');
colorPicker.classList.toggle('hidden', e.target.value !== 'color');
blurSetting.classList.toggle('hidden', e.target.value !== 'blur');
});
function startHoleSelection() {
closeModal('holeModal');
state.isSelecting = true;
state.currentTool = 'hole';
setupSelectionListeners();
alert('在画布上拖动选择要挖孔的区域');
}
// 设置选择监听
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();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
state.selection.width = currentX - startX;
state.selection.height = currentY - 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));
showModal('cropModal');
}
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() {
const shape = document.getElementById('holeShape').value;
const fill = document.getElementById('holeFill').value;
const color = document.getElementById('holeColor').value;
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 if (shape === 'ellipse') {
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 edgeColor = getEdgeColor(x, y, w, h);
ctx.fillStyle = edgeColor;
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();
}
}
addHistory('挖孔填充 (' + shape + ')');
}
// 获取边缘颜色
function getEdgeColor(x, y, w, h) {
// 取左上角附近像素
const pixel = ctx.getImageData(Math.max(0, x - 1), Math.max(0, y - 1), 1, 1).data;
return `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;
}
// 圆形切图
function showCircleModal() {
if (state.images.length === 0) {
alert('请先上传图片');
return;
}
showModal('circleModal');
}
// 监听边缘处理变化
document.getElementById('circleEdge').addEventListener('change', (e) => {
document.getElementById('circleBorderSetting').classList.toggle('hidden', e.target.value !== 'border');
});
function applyCircleCut() {
const diameter = document.getElementById('circleDiameter').value;
const edge = document.getElementById('circleEdge').value;
const borderColor = document.getElementById('circleBorderColor').value;
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') {
// 添加平滑边缘(半透明渐变)
circleCtx.globalCompositeOperation = 'source-over';
const gradient = circleCtx.createRadialGradient(size/2, size/2, size/2 - 10, size/2, size/2, size/2);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
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');
state.images = [];
loadImage(circleData, 'circle_' + new Date().getTime());
closeModal('circleModal');
addHistory('圆形切图 (直径: ' + size + ')');
}
// 文字图片
function showTextModal() {
showModal('textModal');
}
function toggleStyle(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', state.textStyles[style]);
btn.classList.toggle('text-white', state.textStyles[style]);
}
function createTextImage() {
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;
const transparentBg = document.getElementById('textTransparentBg').checked;
const bgColor = document.getElementById('textBgColor').value;
const align = document.getElementById('textAlign').value;
// 计算文字尺寸
const fontStyle = (state.textStyles.bold ? 'bold ' : '') +
(state.textStyles.italic ? 'italic ' : '') +
size + 'px ' + font;
// 创建临时画布测量文字
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCtx.font = fontStyle;
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';
const textAligns = { left: padding, center: totalWidth/2, right: totalWidth - padding };
ctx.textAlign = align;
lines.forEach((line, index) => {
const x = textAligns[align];
const y = padding + index * lineHeight;
ctx.fillText(line, x, y);
});
// 更新状态
const textData = canvas.toDataURL('image/png');
state.images = [];
loadImage(textData, 'text_' + new Date().getTime());
closeModal('textModal');
addHistory('创建文字图片');
}
// 调整大小
function showResizeModal() {
if (state.images.length === 0) {
alert('请先上传图片');
return;
}
const img = state.images[0];
document.getElementById('newWidth').value = img.width;
document.getElementById('newHeight').value = img.height;
showModal('resizeModal');
}
function applyResize() {
const newWidth = parseInt(document.getElementById('newWidth').value);
const newHeight = parseInt(document.getElementById('newHeight').value);
const keepRatio = document.getElementById('keepRatio').checked;
if (!newWidth || !newHeight) {
alert('请输入有效的尺寸');
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');
state.images = [];
loadImage(resizedData, 'resized_' + new Date().getTime());
closeModal('resizeModal');
addHistory(`调整大小 (${newWidth}x${newHeight})`);
}
// 裁剪
function showCropModal() {
if (state.images.length === 0) {
alert('请先上传图片');
return;
}
showModal('cropModal');
}
function startCropSelection() {
closeModal('cropModal');
state.isSelecting = true;
state.currentTool = 'crop';
setupSelectionListeners();
alert('在画布上拖动选择裁剪区域');
}
function applyCrop() {
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');
state.images = [];
loadImage(croppedData, 'cropped_' + new Date().getTime());
closeModal('cropModal');
addHistory(`裁剪 (${w}x${h})`);
}
// 保存图片
function saveImage() {
const dataUrl = canvas.toDataURL('image/png');
const filename = 'edited_' + new Date().getTime() + '.png';
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: dataUrl, filename: filename })
})
.then(res => res.json())
.then(data => {
if (data.success) {
alert('保存成功: ' + data.filename);
addHistory('保存图片: ' + data.filename);
} else {
alert('保存失败');
}
});
}
// 下载图片
function downloadImage() {
const dataUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataUrl;
link.download = 'edited_image_' + new Date().getTime() + '.png';
link.click();
addHistory('下载图片');
}
// 清空画布
function clearCanvas() {
state.images = [];
state.history = [];
setupCanvas();
updateImageList();
updateHistoryList();
showEmptyHint();
document.getElementById('splitResults').classList.add('hidden');
addHistory('清空画布');
}
// 初始化
init();
</script>
</body>
</html>