<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片拼接工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
header p {
opacity: 0.9;
font-size: 1.1em;
}
.main-content {
display: flex;
gap: 30px;
padding: 30px;
}
.left-panel {
flex: 1;
min-width: 300px;
}
.right-panel {
flex: 2;
min-width: 500px;
}
.section {
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
}
.section h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.3em;
display: flex;
align-items: center;
gap: 10px;
}
.section h2::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
/* 上传区域 */
.upload-area {
border: 3px dashed #ddd;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: white;
}
.upload-area:hover {
border-color: #667eea;
background: #f0f4ff;
}
.upload-area.dragover {
border-color: #667eea;
background: #e8eeff;
transform: scale(1.02);
}
.upload-icon {
font-size: 60px;
margin-bottom: 15px;
}
.upload-text {
color: #666;
font-size: 1.1em;
}
.upload-hint {
color: #999;
font-size: 0.9em;
margin-top: 10px;
}
input[type="file"] {
display: none;
}
/* 布局选项 */
.layout-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.layout-option {
background: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.layout-option:hover {
border-color: #667eea;
transform: translateY(-2px);
}
.layout-option.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
}
.layout-preview {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
margin-bottom: 10px;
}
.layout-preview .box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
}
/* 上下排列 */
.layout-v .box {
width: 30px;
height: 15px;
margin: 2px 0;
}
/* 左右排列 */
.layout-h .box {
width: 15px;
height: 30px;
margin: 0 2px;
}
/* 2x2 网格 */
.layout-2x2 {
display: grid !important;
grid-template-columns: repeat(2, 1fr);
gap: 3px;
width: fit-content;
}
.layout-2x2 .box {
width: 18px;
height: 18px;
}
/* 3x3 网格 */
.layout-3x3 {
display: grid !important;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
width: fit-content;
}
.layout-3x3 .box {
width: 12px;
height: 12px;
}
/* 4x4 网格 */
.layout-4x4 {
display: grid !important;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
width: fit-content;
}
.layout-4x4 .box {
width: 10px;
height: 10px;
}
.layout-name {
font-weight: 600;
color: #333;
font-size: 0.95em;
}
/* 间距控制 */
.gap-control {
margin-top: 15px;
}
.gap-control label {
display: block;
margin-bottom: 10px;
color: #555;
font-weight: 500;
}
.gap-control input[type="range"] {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
}
.gap-control input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.gap-value {
text-align: center;
margin-top: 8px;
color: #667eea;
font-weight: 600;
}
/* 背景色控制 */
.bg-control {
margin-top: 20px;
}
.bg-control label {
display: block;
margin-bottom: 10px;
color: #555;
font-weight: 500;
}
.color-options {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.color-option {
width: 35px;
height: 35px;
border-radius: 8px;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.3s ease;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: #333;
}
/* 图片列表 */
.image-list {
max-height: 300px;
overflow-y: auto;
padding-right: 10px;
}
.image-list::-webkit-scrollbar {
width: 8px;
}
.image-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.image-list::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 4px;
}
.image-item {
display: flex;
align-items: center;
gap: 15px;
background: white;
padding: 12px;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.image-item img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 8px;
}
.image-info {
flex: 1;
overflow: hidden;
}
.image-name {
font-weight: 600;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-size {
color: #888;
font-size: 0.85em;
margin-top: 3px;
}
.image-actions {
display: flex;
gap: 5px;
}
.btn-icon {
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-size: 16px;
}
.btn-move {
background: #e8f4fd;
color: #2196F3;
}
.btn-move:hover {
background: #2196F3;
color: white;
}
.btn-delete {
background: #ffebee;
color: #f44336;
}
.btn-delete:hover {
background: #f44336;
color: white;
}
/* 预览区域 */
.preview-area {
background: #f5f5f5;
border-radius: 15px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: 20px;
}
.preview-placeholder {
text-align: center;
color: #999;
}
.preview-placeholder .icon {
font-size: 80px;
margin-bottom: 20px;
}
#previewCanvas {
max-width: 100%;
max-height: 600px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
/* 按钮样式 */
.btn {
padding: 15px 30px;
border: none;
border-radius: 12px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state .icon {
font-size: 60px;
margin-bottom: 15px;
}
/* 响应式 */
@media (max-width: 900px) {
.main-content {
flex-direction: column;
}
.left-panel, .right-panel {
min-width: 100%;
}
}
/* 动画 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading {
animation: pulse 1.5s infinite;
}
/* 图片尺寸设置 */
.size-control {
margin-top: 20px;
}
.size-control label {
display: block;
margin-bottom: 10px;
color: #555;
font-weight: 500;
}
.size-inputs {
display: flex;
gap: 15px;
align-items: center;
}
.size-input {
flex: 1;
}
.size-input input {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
.size-input input:focus {
outline: none;
border-color: #667eea;
}
.size-separator {
font-size: 1.2em;
color: #999;
}
.size-locked {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.size-locked input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.size-locked label {
cursor: pointer;
margin: 0;
}
/* 输出格式选择 */
.format-control {
margin-top: 20px;
}
.format-control label {
display: block;
margin-bottom: 10px;
color: #555;
font-weight: 500;
}
.format-options {
display: flex;
gap: 10px;
}
.format-option {
flex: 1;
padding: 12px;
background: white;
border: 2px solid #e0e0e0;
border-radius: 10px;
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
}
.format-option:hover {
border-color: #667eea;
}
.format-option.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
}
.format-option input {
display: none;
}
.format-option span {
font-weight: 600;
color: #333;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🖼️ 图片拼接工具</h1>
<p>支持多种排列方式,无缝拼接,一键导出</p>
</header>
<div class="main-content">
<div class="left-panel">
<!-- 上传区域 -->
<div class="section">
<h2>上传图片</h2>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽图片到这里</div>
<div class="upload-hint">支持 JPG、PNG、GIF、WebP 格式</div>
</div>
<input type="file" id="fileInput" accept="image/*" multiple>
</div>
<!-- 布局设置 -->
<div class="section">
<h2>布局方式</h2>
<div class="layout-options">
<div class="layout-option active" data-layout="vertical">
<div class="layout-preview layout-v">
<div class="box"></div>
<div class="box"></div>
</div>
<div class="layout-name">上 → 下</div>
</div>
<div class="layout-option" data-layout="horizontal">
<div class="layout-preview layout-h">
<div class="box"></div>
<div class="box"></div>
</div>
<div class="layout-name">左 → 右</div>
</div>
<div class="layout-option" data-layout="2x2">
<div class="layout-preview layout-2x2">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
<div class="layout-name">2 × 2</div>
</div>
<div class="layout-option" data-layout="3x3">
<div class="layout-preview layout-3x3">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
<div class="layout-name">3 × 3</div>
</div>
<div class="layout-option" data-layout="4x4">
<div class="layout-preview layout-4x4">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
<div class="layout-name">4 × 4</div>
</div>
</div>
<!-- 间距控制 -->
<div class="gap-control">
<label>图片间距:0 px(无缝拼接)</label>
<input type="range" id="gapRange" min="0" max="50" value="0">
<div class="gap-value" id="gapValue">0 px</div>
</div>
<!-- 背景色控制 -->
<div class="bg-control">
<label>背景颜色</label>
<div class="color-options">
<div class="color-option active" style="background: #ffffff;" data-color="#ffffff"></div>
<div class="color-option" style="background: #000000;" data-color="#000000"></div>
<div class="color-option" style="background: #f5f5f5;" data-color="#f5f5f5"></div>
<div class="color-option" style="background: #333333;" data-color="#333333"></div>
<div class="color-option" style="background: #ff6b6b;" data-color="#ff6b6b"></div>
<div class="color-option" style="background: #4ecdc4;" data-color="#4ecdc4"></div>
<div class="color-option" style="background: #667eea;" data-color="#667eea"></div>
<div class="color-option" style="background: #764ba2;" data-color="#764ba2"></div>
</div>
</div>
<!-- 输出格式 -->
<div class="format-control">
<label>输出格式</label>
<div class="format-options">
<label class="format-option active" data-format="png">
<input type="radio" name="format" value="png" checked>
<span>PNG</span>
</label>
<label class="format-option" data-format="jpeg">
<input type="radio" name="format" value="jpeg">
<span>JPEG</span>
</label>
<label class="format-option" data-format="webp">
<input type="radio" name="format" value="webp">
<span>WebP</span>
</label>
</div>
</div>
</div>
<!-- 图片列表 -->
<div class="section">
<h2>图片列表 <span id="imageCount">(0)</span></h2>
<div class="image-list" id="imageList">
<div class="empty-state">
<div class="icon">📷</div>
<div>还没有添加图片</div>
</div>
</div>
</div>
</div>
<div class="right-panel">
<div class="section" style="min-height: 500px;">
<h2>预览效果</h2>
<div class="preview-area" id="previewArea">
<div class="preview-placeholder" id="previewPlaceholder">
<div class="icon">🖼️</div>
<div>上传图片后预览拼接效果</div>
</div>
<canvas id="previewCanvas" style="display: none;"></canvas>
</div>
<div class="button-group">
<button class="btn btn-primary" id="mergeBtn" disabled>
🎨 生成拼接图片
</button>
<button class="btn btn-secondary" id="downloadBtn" disabled>
💾 下载图片
</button>
<button class="btn btn-secondary" id="clearBtn">
🗑️ 清空图片
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let images = [];
let currentLayout = 'vertical';
let gap = 0;
let bgColor = '#ffffff';
let outputFormat = 'png';
let mergedImageData = null;
// DOM 元素
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const imageList = document.getElementById('imageList');
const imageCount = document.getElementById('imageCount');
const previewArea = document.getElementById('previewArea');
const previewPlaceholder = document.getElementById('previewPlaceholder');
const previewCanvas = document.getElementById('previewCanvas');
const mergeBtn = document.getElementById('mergeBtn');
const downloadBtn = document.getElementById('downloadBtn');
const clearBtn = document.getElementById('clearBtn');
const gapRange = document.getElementById('gapRange');
const gapValue = document.getElementById('gapValue');
// 上传事件
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
fileInput.value = '';
});
// 处理文件
function handleFiles(files) {
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
images.push({
id: Date.now() + Math.random(),
name: file.name,
size: file.size,
width: img.width,
height: img.height,
src: e.target.result,
img: img
});
updateUI();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
}
// 更新UI
function updateUI() {
imageCount.textContent = `(${images.length})`;
mergeBtn.disabled = images.length === 0;
if (images.length === 0) {
imageList.innerHTML = `
<div class="empty-state">
<div class="icon">📷</div>
<div>还没有添加图片</div>
</div>
`;
previewPlaceholder.style.display = 'block';
previewCanvas.style.display = 'none';
} else {
renderImageList();
previewMerge();
}
}
// 渲染图片列表
function renderImageList() {
imageList.innerHTML = images.map((img, index) => `
<div class="image-item" data-id="${img.id}">
<img src="${img.src}" alt="${img.name}">
<div class="image-info">
<div class="image-name">${img.name}</div>
<div class="image-size">${img.width} × ${img.height} · ${formatSize(img.size)}</div>
</div>
<div class="image-actions">
<button class="btn-icon btn-move" onclick="moveImage(${index}, -1)" ${index === 0 ? 'disabled' : ''}>↑</button>
<button class="btn-icon btn-move" onclick="moveImage(${index}, 1)" ${index === images.length - 1 ? 'disabled' : ''}>↓</button>
<button class="btn-icon btn-delete" onclick="removeImage(${index})">×</button>
</div>
</div>
`).join('');
}
// 移动图片
function moveImage(index, direction) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= images.length) return;
[images[index], images[newIndex]] = [images[newIndex], images[index]];
renderImageList();
previewMerge();
}
// 删除图片
function removeImage(index) {
images.splice(index, 1);
updateUI();
}
// 格式化文件大小
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// 布局选择
document.querySelectorAll('.layout-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.layout-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
currentLayout = option.dataset.layout;
previewMerge();
});
});
// 间距控制
gapRange.addEventListener('input', (e) => {
gap = parseInt(e.target.value);
gapValue.textContent = gap + ' px';
previewMerge();
});
// 背景色选择
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
bgColor = option.dataset.color;
previewMerge();
});
});
// 输出格式选择
document.querySelectorAll('.format-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.format-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
outputFormat = option.dataset.format;
});
});
// 预览拼接
function previewMerge() {
if (images.length === 0) {
previewPlaceholder.style.display = 'block';
previewCanvas.style.display = 'none';
return;
}
const ctx = previewCanvas.getContext('2d');
const result = calculateLayout();
previewCanvas.width = result.canvasWidth;
previewCanvas.height = result.canvasHeight;
previewCanvas.style.display = 'block';
previewPlaceholder.style.display = 'none';
// 绘制背景
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, result.canvasWidth, result.canvasHeight);
// 绘制图片
result.positions.forEach(pos => {
const img = images[pos.index];
if (img && img.img) {
ctx.drawImage(img.img, pos.x, pos.y, pos.width, pos.height);
}
});
mergedImageData = result;
}
// 计算布局
function calculateLayout() {
const positions = [];
let canvasWidth = 0;
let canvasHeight = 0;
if (currentLayout === 'vertical') {
// 上下排列 - 统一宽度
const maxWidth = Math.max(...images.map(img => img.width));
let y = 0;
images.forEach((img, index) => {
const scale = maxWidth / img.width;
const width = maxWidth;
const height = img.height * scale;
positions.push({ index, x: 0, y, width, height });
y += height + gap;
canvasWidth = maxWidth;
});
canvasHeight = y - gap;
} else if (currentLayout === 'horizontal') {
// 左右排列 - 统一高度
const maxHeight = Math.max(...images.map(img => img.height));
let x = 0;
images.forEach((img, index) => {
const scale = maxHeight / img.height;
const width = img.width * scale;
const height = maxHeight;
positions.push({ index, x, y: 0, width, height });
x += width + gap;
});
canvasWidth = x - gap;
canvasHeight = maxHeight;
} else {
// 网格排列
const gridSize = parseInt(currentLayout.split('x')[0]);
const cellWidth = Math.max(...images.map(img => img.width));
const cellHeight = Math.max(...images.map(img => img.height));
images.forEach((img, index) => {
const row = Math.floor(index / gridSize);
const col = index % gridSize;
const x = col * (cellWidth + gap);
const y = row * (cellHeight + gap);
positions.push({ index, x, y, width: cellWidth, height: cellHeight });
});
const rows = Math.ceil(images.length / gridSize);
canvasWidth = gridSize * cellWidth + (gridSize - 1) * gap;
canvasHeight = rows * cellHeight + (rows - 1) * gap;
}
return { canvasWidth, canvasHeight, positions };
}
// 生成拼接图片
mergeBtn.addEventListener('click', () => {
previewMerge();
downloadBtn.disabled = false;
});
// 下载图片
downloadBtn.addEventListener('click', () => {
if (!mergedImageData) return;
const mimeType = `image/${outputFormat}`;
const extension = outputFormat === 'jpeg' ? 'jpg' : outputFormat;
const filename = `merged_${Date.now()}.${extension}`;
const link = document.createElement('a');
link.download = filename;
link.href = previewCanvas.toDataURL(mimeType, 0.95);
link.click();
});
// 清空图片
clearBtn.addEventListener('click', () => {
images = [];
updateUI();
downloadBtn.disabled = true;
});
// 初始加载提示
console.log('🖼️ 图片拼接工具已就绪');
</script>
</body>
</html>
现成的不能符合我的需求,我就Vibe了一个,需要的拿走不谢
1 个帖子 - 1 位参与者