|
|
@@ -0,0 +1,956 @@
|
|
|
+<!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: 'Microsoft YaHei', Arial, sans-serif;
|
|
|
+ background: #f5f5f5;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ display: flex;
|
|
|
+ height: 100vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar {
|
|
|
+ width: 250px;
|
|
|
+ background: #2c3e50;
|
|
|
+ color: white;
|
|
|
+ padding: 20px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar h1 {
|
|
|
+ font-size: 18px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ text-align: center;
|
|
|
+ color: #ecf0f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-section {
|
|
|
+ margin-bottom: 25px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-section h3 {
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #bdc3c7;
|
|
|
+ border-bottom: 1px solid #34495e;
|
|
|
+ padding-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ margin: 5px 0;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-primary {
|
|
|
+ background: #3498db;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-primary:hover {
|
|
|
+ background: #2980b9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-success {
|
|
|
+ background: #27ae60;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-success:hover {
|
|
|
+ background: #229954;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-warning {
|
|
|
+ background: #f39c12;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-warning:hover {
|
|
|
+ background: #e67e22;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-danger {
|
|
|
+ background: #e74c3c;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-danger:hover {
|
|
|
+ background: #c0392b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-input {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .canvas-container {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ecf0f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .canvas {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: white;
|
|
|
+ border: 2px solid #bdc3c7;
|
|
|
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
|
+ cursor: grab;
|
|
|
+ }
|
|
|
+
|
|
|
+ .canvas:active {
|
|
|
+ cursor: grabbing;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-item {
|
|
|
+ position: absolute;
|
|
|
+ cursor: move;
|
|
|
+ border: 2px solid transparent;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-item:hover {
|
|
|
+ border-color: #3498db;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-item.selected {
|
|
|
+ border-color: #e74c3c;
|
|
|
+ box-shadow: 0 0 10px rgba(231, 76, 60, 0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-item img {
|
|
|
+ display: block;
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ background: rgba(44, 62, 80, 0.9);
|
|
|
+ color: white;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ min-width: 200px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .zoom-controls {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 20px;
|
|
|
+ right: 20px;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .zoom-btn {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #34495e;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 18px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .zoom-btn:hover {
|
|
|
+ background: #2c3e50;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selection-box {
|
|
|
+ position: absolute;
|
|
|
+ border: 2px dashed #3498db;
|
|
|
+ background: rgba(52, 152, 219, 0.1);
|
|
|
+ pointer-events: none;
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-bar {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 250px;
|
|
|
+ right: 0;
|
|
|
+ height: 30px;
|
|
|
+ background: #34495e;
|
|
|
+ color: white;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 15px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <div class="toolbar">
|
|
|
+ <h1>图片拼接工具</h1>
|
|
|
+
|
|
|
+ <div class="tool-section">
|
|
|
+ <h3>文件操作</h3>
|
|
|
+ <button class="btn btn-primary" onclick="importImages()">导入图片</button>
|
|
|
+ <input type="file" id="fileInput" class="file-input" multiple accept="image/*">
|
|
|
+ <button class="btn btn-success" onclick="exportCanvas()">导出内容</button>
|
|
|
+ <button class="btn btn-success" onclick="exportFullCanvas()">导出画布</button>
|
|
|
+ <button class="btn btn-warning" onclick="fitCanvasToContent()">缩小画布</button>
|
|
|
+ <button class="btn btn-danger" onclick="clearCanvas()">清空画布</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tool-section">
|
|
|
+ <h3>选择操作</h3>
|
|
|
+ <button class="btn btn-primary" onclick="selectAll()">全选</button>
|
|
|
+ <button class="btn btn-primary" onclick="clearSelection()">取消选择</button>
|
|
|
+ <button class="btn btn-danger" onclick="deleteSelected()">删除选中</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tool-section">
|
|
|
+ <h3>排列工具</h3>
|
|
|
+ <label style="color: #bdc3c7; font-size: 12px;">水平对齐方式:</label>
|
|
|
+ <select id="alignMode" style="width: 100%; margin: 5px 0; padding: 5px;">
|
|
|
+ <option value="center">中心对齐</option>
|
|
|
+ <option value="top">顶边对齐</option>
|
|
|
+ <option value="bottom">底边对齐</option>
|
|
|
+ </select>
|
|
|
+ <button class="btn btn-warning" onclick="alignHorizontal()">水平排列</button>
|
|
|
+ <button class="btn btn-warning" onclick="alignVertical()">垂直排列</button>
|
|
|
+ <button class="btn btn-warning" onclick="distributeHorizontal()">水平分布</button>
|
|
|
+ <button class="btn btn-warning" onclick="distributeVertical()">垂直分布</button>
|
|
|
+ <label style="color: #bdc3c7; font-size: 12px; margin-top: 10px; display: block;">中心点间距:</label>
|
|
|
+ <input type="number" id="customSpacing" value="100" style="width: 100%; margin: 5px 0; padding: 5px;" placeholder="中心点间距(像素)">
|
|
|
+ <button class="btn btn-warning" onclick="distributeHorizontalCustom()">按中心间距分布</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tool-section">
|
|
|
+ <h3>画布设置</h3>
|
|
|
+ <label style="color: #bdc3c7; font-size: 12px;">宽度:</label>
|
|
|
+ <input type="number" id="canvasWidth" value="800" style="width: 100%; margin: 5px 0; padding: 5px;">
|
|
|
+ <label style="color: #bdc3c7; font-size: 12px;">高度:</label>
|
|
|
+ <input type="number" id="canvasHeight" value="600" style="width: 100%; margin: 5px 0; padding: 5px;">
|
|
|
+ <button class="btn btn-primary" onclick="resizeCanvas()">调整画布</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="canvas-container">
|
|
|
+ <div class="canvas" id="canvas"></div>
|
|
|
+ <div class="selection-box" id="selectionBox"></div>
|
|
|
+
|
|
|
+ <div class="info-panel">
|
|
|
+ <div>画布尺寸: <span id="canvasSize">800 x 600</span></div>
|
|
|
+ <div>图片数量: <span id="imageCount">0</span></div>
|
|
|
+ <div>选中数量: <span id="selectedCount">0</span></div>
|
|
|
+ <div>缩放比例: <span id="zoomLevel">100%</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="zoom-controls">
|
|
|
+ <button class="zoom-btn" onclick="zoomIn()">+</button>
|
|
|
+ <button class="zoom-btn" onclick="zoomOut()">-</button>
|
|
|
+ <button class="zoom-btn" onclick="resetZoom()">⌂</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="status-bar">
|
|
|
+ <span id="statusText">就绪</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ class ImageComposer {
|
|
|
+ constructor() {
|
|
|
+ this.canvas = document.getElementById('canvas');
|
|
|
+ this.images = [];
|
|
|
+ this.selectedImages = [];
|
|
|
+ this.isDragging = false;
|
|
|
+ this.isSelecting = false;
|
|
|
+ this.dragOffset = { x: 0, y: 0 };
|
|
|
+ this.canvasOffset = { x: 0, y: 0 };
|
|
|
+ this.zoom = 1;
|
|
|
+ this.canvasWidth = 800;
|
|
|
+ this.canvasHeight = 600;
|
|
|
+
|
|
|
+ this.init();
|
|
|
+ }
|
|
|
+
|
|
|
+ init() {
|
|
|
+ this.setupCanvas();
|
|
|
+ this.setupEventListeners();
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ setupCanvas() {
|
|
|
+ this.canvas.style.width = this.canvasWidth + 'px';
|
|
|
+ this.canvas.style.height = this.canvasHeight + 'px';
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ setupEventListeners() {
|
|
|
+ // 文件输入
|
|
|
+ document.getElementById('fileInput').addEventListener('change', (e) => {
|
|
|
+ this.handleFileSelect(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 画布拖拽
|
|
|
+ this.canvas.addEventListener('mousedown', (e) => {
|
|
|
+ if (e.target === this.canvas) {
|
|
|
+ this.startCanvasDrag(e);
|
|
|
+ } else if (e.target.closest('.image-item')) {
|
|
|
+ this.startImageDrag(e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 全局鼠标事件
|
|
|
+ document.addEventListener('mousemove', (e) => {
|
|
|
+ this.handleMouseMove(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('mouseup', (e) => {
|
|
|
+ this.handleMouseUp(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 键盘事件
|
|
|
+ document.addEventListener('keydown', (e) => {
|
|
|
+ this.handleKeyDown(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 防止拖拽默认行为
|
|
|
+ this.canvas.addEventListener('dragover', (e) => e.preventDefault());
|
|
|
+ this.canvas.addEventListener('drop', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ this.handleFileDrop(e);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ handleFileSelect(e) {
|
|
|
+ const files = Array.from(e.target.files);
|
|
|
+ this.loadImages(files);
|
|
|
+ }
|
|
|
+
|
|
|
+ handleFileDrop(e) {
|
|
|
+ const files = Array.from(e.dataTransfer.files).filter(file =>
|
|
|
+ file.type.startsWith('image/')
|
|
|
+ );
|
|
|
+ this.loadImages(files);
|
|
|
+ }
|
|
|
+
|
|
|
+ loadImages(files) {
|
|
|
+ files.forEach(file => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.addImage(e.target.result, file.name);
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ addImage(src, name) {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => {
|
|
|
+ // 计算新图片的位置:放在最右边
|
|
|
+ let newX = 10; // 默认起始位置
|
|
|
+ let newY = 10;
|
|
|
+
|
|
|
+ if (this.images.length > 0) {
|
|
|
+ // 找到最右边的图片
|
|
|
+ const rightmostX = Math.max(...this.images.map(item => item.x + item.width));
|
|
|
+ newX = rightmostX + 20; // 在最右边留20px间距
|
|
|
+
|
|
|
+ // Y坐标与最后一个图片对齐,如果超出画布则重置为顶部
|
|
|
+ const lastImage = this.images[this.images.length - 1];
|
|
|
+ newY = lastImage.y;
|
|
|
+
|
|
|
+ // 如果新位置超出画布,则换行
|
|
|
+ if (newX + img.width > this.canvasWidth) {
|
|
|
+ newX = 10;
|
|
|
+ // 找到最底部的图片,在其下方放置
|
|
|
+ const bottomY = Math.max(...this.images.map(item => item.y + item.height));
|
|
|
+ newY = bottomY + 20;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保不超出画布边界
|
|
|
+ newX = Math.max(0, Math.min(newX, this.canvasWidth - img.width));
|
|
|
+ newY = Math.max(0, Math.min(newY, this.canvasHeight - img.height));
|
|
|
+
|
|
|
+ const imageItem = {
|
|
|
+ id: Date.now() + Math.random(),
|
|
|
+ element: this.createImageElement(src, name),
|
|
|
+ x: newX,
|
|
|
+ y: newY,
|
|
|
+ width: img.width,
|
|
|
+ height: img.height,
|
|
|
+ name: name
|
|
|
+ };
|
|
|
+
|
|
|
+ this.images.push(imageItem);
|
|
|
+ this.canvas.appendChild(imageItem.element);
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ this.updateInfo();
|
|
|
+ this.setStatus(`已添加图片: ${name}`);
|
|
|
+ };
|
|
|
+ img.src = src;
|
|
|
+ }
|
|
|
+
|
|
|
+ createImageElement(src, name) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.className = 'image-item';
|
|
|
+ div.innerHTML = `<img src="${src}" alt="${name}" title="${name}">`;
|
|
|
+
|
|
|
+ div.addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ if (e.ctrlKey) {
|
|
|
+ this.toggleImageSelection(div);
|
|
|
+ } else {
|
|
|
+ this.selectImage(div);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return div;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateImagePosition(imageItem) {
|
|
|
+ imageItem.element.style.left = imageItem.x + 'px';
|
|
|
+ imageItem.element.style.top = imageItem.y + 'px';
|
|
|
+ }
|
|
|
+
|
|
|
+ startImageDrag(e) {
|
|
|
+ const imageElement = e.target.closest('.image-item');
|
|
|
+ const imageItem = this.images.find(img => img.element === imageElement);
|
|
|
+
|
|
|
+ if (!imageItem) return;
|
|
|
+
|
|
|
+ if (!this.selectedImages.includes(imageItem)) {
|
|
|
+ this.selectImage(imageElement);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isDragging = true;
|
|
|
+ const rect = this.canvas.getBoundingClientRect();
|
|
|
+ this.dragOffset = {
|
|
|
+ x: e.clientX - rect.left - imageItem.x,
|
|
|
+ y: e.clientY - rect.top - imageItem.y
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ startCanvasDrag(e) {
|
|
|
+ this.clearSelection();
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMouseMove(e) {
|
|
|
+ if (this.isDragging && this.selectedImages.length > 0) {
|
|
|
+ const rect = this.canvas.getBoundingClientRect();
|
|
|
+ const newX = e.clientX - rect.left - this.dragOffset.x;
|
|
|
+ const newY = e.clientY - rect.top - this.dragOffset.y;
|
|
|
+
|
|
|
+ const deltaX = newX - this.selectedImages[0].x;
|
|
|
+ const deltaY = newY - this.selectedImages[0].y;
|
|
|
+
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ imageItem.x += deltaX;
|
|
|
+ imageItem.y += deltaY;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMouseUp(e) {
|
|
|
+ this.isDragging = false;
|
|
|
+ this.isSelecting = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ handleKeyDown(e) {
|
|
|
+ if (e.key === 'Delete') {
|
|
|
+ this.deleteSelected();
|
|
|
+ } else if (e.ctrlKey && e.key === 'a') {
|
|
|
+ e.preventDefault();
|
|
|
+ this.selectAll();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ selectImage(element) {
|
|
|
+ this.clearSelection();
|
|
|
+ const imageItem = this.images.find(img => img.element === element);
|
|
|
+ if (imageItem) {
|
|
|
+ this.selectedImages = [imageItem];
|
|
|
+ element.classList.add('selected');
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleImageSelection(element) {
|
|
|
+ const imageItem = this.images.find(img => img.element === element);
|
|
|
+ if (!imageItem) return;
|
|
|
+
|
|
|
+ const index = this.selectedImages.indexOf(imageItem);
|
|
|
+ if (index > -1) {
|
|
|
+ this.selectedImages.splice(index, 1);
|
|
|
+ element.classList.remove('selected');
|
|
|
+ } else {
|
|
|
+ this.selectedImages.push(imageItem);
|
|
|
+ element.classList.add('selected');
|
|
|
+ }
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ selectAll() {
|
|
|
+ this.selectedImages = [...this.images];
|
|
|
+ this.images.forEach(img => img.element.classList.add('selected'));
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ clearSelection() {
|
|
|
+ this.selectedImages = [];
|
|
|
+ this.images.forEach(img => img.element.classList.remove('selected'));
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ deleteSelected() {
|
|
|
+ if (this.selectedImages.length === 0) return;
|
|
|
+
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ this.canvas.removeChild(imageItem.element);
|
|
|
+ const index = this.images.indexOf(imageItem);
|
|
|
+ if (index > -1) {
|
|
|
+ this.images.splice(index, 1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.selectedImages = [];
|
|
|
+ this.updateInfo();
|
|
|
+ this.setStatus(`已删除 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+
|
|
|
+ alignHorizontal() {
|
|
|
+ if (this.selectedImages.length < 2) {
|
|
|
+ this.setStatus('请选择至少2个图片进行排列');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按X坐标排序
|
|
|
+ this.selectedImages.sort((a, b) => a.x - b.x);
|
|
|
+
|
|
|
+ const alignMode = document.getElementById('alignMode').value;
|
|
|
+ let referenceY;
|
|
|
+
|
|
|
+ if (alignMode === 'center') {
|
|
|
+ // 中心对齐:计算平均中心Y坐标
|
|
|
+ const avgCenterY = this.selectedImages.reduce((sum, img) => sum + img.y + img.height / 2, 0) / this.selectedImages.length;
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ imageItem.y = avgCenterY - imageItem.height / 2;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+ this.setStatus(`已按中心水平对齐 ${this.selectedImages.length} 个图片`);
|
|
|
+ } else if (alignMode === 'top') {
|
|
|
+ // 顶边对齐:找到最小Y坐标
|
|
|
+ const minY = Math.min(...this.selectedImages.map(img => img.y));
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ imageItem.y = minY;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+ this.setStatus(`已按顶边水平对齐 ${this.selectedImages.length} 个图片`);
|
|
|
+ } else if (alignMode === 'bottom') {
|
|
|
+ // 底边对齐:找到最大底边Y坐标
|
|
|
+ const maxBottomY = Math.max(...this.selectedImages.map(img => img.y + img.height));
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ imageItem.y = maxBottomY - imageItem.height;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+ this.setStatus(`已按底边水平对齐 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ alignVertical() {
|
|
|
+ if (this.selectedImages.length < 2) {
|
|
|
+ this.setStatus('请选择至少2个图片进行排列');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按Y坐标排序
|
|
|
+ this.selectedImages.sort((a, b) => a.y - b.y);
|
|
|
+
|
|
|
+ // 计算平均X坐标
|
|
|
+ const avgX = this.selectedImages.reduce((sum, img) => sum + img.x, 0) / this.selectedImages.length;
|
|
|
+
|
|
|
+ // 设置相同的X坐标
|
|
|
+ this.selectedImages.forEach(imageItem => {
|
|
|
+ imageItem.x = avgX;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.setStatus(`已垂直对齐 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+
|
|
|
+ distributeHorizontal() {
|
|
|
+ if (this.selectedImages.length < 3) {
|
|
|
+ this.setStatus('请选择至少3个图片进行分布');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按X坐标排序
|
|
|
+ this.selectedImages.sort((a, b) => a.x - b.x);
|
|
|
+
|
|
|
+ const first = this.selectedImages[0];
|
|
|
+ const last = this.selectedImages[this.selectedImages.length - 1];
|
|
|
+ const totalDistance = last.x - first.x;
|
|
|
+ const spacing = totalDistance / (this.selectedImages.length - 1);
|
|
|
+
|
|
|
+ this.selectedImages.forEach((imageItem, index) => {
|
|
|
+ imageItem.x = first.x + spacing * index;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.setStatus(`已水平分布 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+
|
|
|
+ distributeVertical() {
|
|
|
+ if (this.selectedImages.length < 3) {
|
|
|
+ this.setStatus('请选择至少3个图片进行分布');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按Y坐标排序
|
|
|
+ this.selectedImages.sort((a, b) => a.y - b.y);
|
|
|
+
|
|
|
+ const first = this.selectedImages[0];
|
|
|
+ const last = this.selectedImages[this.selectedImages.length - 1];
|
|
|
+ const totalDistance = last.y - first.y;
|
|
|
+ const spacing = totalDistance / (this.selectedImages.length - 1);
|
|
|
+
|
|
|
+ this.selectedImages.forEach((imageItem, index) => {
|
|
|
+ imageItem.y = first.y + spacing * index;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.setStatus(`已垂直分布 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+
|
|
|
+ distributeHorizontalCustom() {
|
|
|
+ if (this.selectedImages.length < 2) {
|
|
|
+ this.setStatus('请选择至少2个图片进行分布');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const customSpacing = parseInt(document.getElementById('customSpacing').value);
|
|
|
+ if (isNaN(customSpacing) || customSpacing <= 0) {
|
|
|
+ this.setStatus('请输入有效的中心点间距值');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按X坐标排序(按中心点排序)
|
|
|
+ this.selectedImages.sort((a, b) => (a.x + a.width / 2) - (b.x + b.width / 2));
|
|
|
+
|
|
|
+ // 从第一个图片的中心点开始,按指定中心点间距排列
|
|
|
+ const firstCenterX = this.selectedImages[0].x + this.selectedImages[0].width / 2;
|
|
|
+
|
|
|
+ this.selectedImages.forEach((imageItem, index) => {
|
|
|
+ if (index === 0) {
|
|
|
+ // 第一个图片保持原位置
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ // 后续图片按中心点间距排列
|
|
|
+ const newCenterX = firstCenterX + customSpacing * index;
|
|
|
+ imageItem.x = newCenterX - imageItem.width / 2;
|
|
|
+ }
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.setStatus(`已按 ${customSpacing}px 中心点间距水平分布 ${this.selectedImages.length} 个图片`);
|
|
|
+ }
|
|
|
+
|
|
|
+ resizeCanvas() {
|
|
|
+ const width = parseInt(document.getElementById('canvasWidth').value);
|
|
|
+ const height = parseInt(document.getElementById('canvasHeight').value);
|
|
|
+
|
|
|
+ if (width > 0 && height > 0) {
|
|
|
+ this.canvasWidth = width;
|
|
|
+ this.canvasHeight = height;
|
|
|
+ this.setupCanvas();
|
|
|
+ this.setStatus(`画布已调整为 ${width} x ${height}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ clearCanvas() {
|
|
|
+ if (confirm('确定要清空画布吗?')) {
|
|
|
+ this.images.forEach(img => this.canvas.removeChild(img.element));
|
|
|
+ this.images = [];
|
|
|
+ this.selectedImages = [];
|
|
|
+ this.updateInfo();
|
|
|
+ this.setStatus('画布已清空');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ exportCanvas() {
|
|
|
+ if (this.images.length === 0) {
|
|
|
+ this.setStatus('画布为空,无法导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算实际内容边界
|
|
|
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
|
+
|
|
|
+ this.images.forEach(imageItem => {
|
|
|
+ minX = Math.min(minX, imageItem.x);
|
|
|
+ minY = Math.min(minY, imageItem.y);
|
|
|
+ maxX = Math.max(maxX, imageItem.x + imageItem.width);
|
|
|
+ maxY = Math.max(maxY, imageItem.y + imageItem.height);
|
|
|
+ });
|
|
|
+
|
|
|
+ const contentWidth = maxX - minX;
|
|
|
+ const contentHeight = maxY - minY;
|
|
|
+
|
|
|
+ // 创建临时canvas
|
|
|
+ const tempCanvas = document.createElement('canvas');
|
|
|
+ const ctx = tempCanvas.getContext('2d');
|
|
|
+ tempCanvas.width = contentWidth;
|
|
|
+ tempCanvas.height = contentHeight;
|
|
|
+
|
|
|
+ // 绘制所有图片
|
|
|
+ let loadedCount = 0;
|
|
|
+ const totalImages = this.images.length;
|
|
|
+
|
|
|
+ this.images.forEach(imageItem => {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => {
|
|
|
+ ctx.drawImage(img, imageItem.x - minX, imageItem.y - minY, imageItem.width, imageItem.height);
|
|
|
+ loadedCount++;
|
|
|
+
|
|
|
+ if (loadedCount === totalImages) {
|
|
|
+ // 所有图片加载完成,导出
|
|
|
+ tempCanvas.toBlob(blob => {
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = `composed_content_${Date.now()}.png`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ this.setStatus('内容已导出');
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ img.src = imageItem.element.querySelector('img').src;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ exportFullCanvas() {
|
|
|
+ if (this.images.length === 0) {
|
|
|
+ this.setStatus('画布为空,无法导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建临时canvas,使用完整画布大小
|
|
|
+ const tempCanvas = document.createElement('canvas');
|
|
|
+ const ctx = tempCanvas.getContext('2d');
|
|
|
+ tempCanvas.width = this.canvasWidth;
|
|
|
+ tempCanvas.height = this.canvasHeight;
|
|
|
+
|
|
|
+ // 背景保持透明,不填充任何颜色
|
|
|
+
|
|
|
+ // 绘制所有图片
|
|
|
+ let loadedCount = 0;
|
|
|
+ const totalImages = this.images.length;
|
|
|
+
|
|
|
+ this.images.forEach(imageItem => {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => {
|
|
|
+ ctx.drawImage(img, imageItem.x, imageItem.y, imageItem.width, imageItem.height);
|
|
|
+ loadedCount++;
|
|
|
+
|
|
|
+ if (loadedCount === totalImages) {
|
|
|
+ // 所有图片加载完成,导出
|
|
|
+ tempCanvas.toBlob(blob => {
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = `full_canvas_${Date.now()}.png`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ this.setStatus('完整画布已导出');
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ img.src = imageItem.element.querySelector('img').src;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ fitCanvasToContent() {
|
|
|
+ if (this.images.length === 0) {
|
|
|
+ this.setStatus('没有图片,无法调整画布大小');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算所有图片的边界
|
|
|
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
|
+
|
|
|
+ this.images.forEach(imageItem => {
|
|
|
+ minX = Math.min(minX, imageItem.x);
|
|
|
+ minY = Math.min(minY, imageItem.y);
|
|
|
+ maxX = Math.max(maxX, imageItem.x + imageItem.width);
|
|
|
+ maxY = Math.max(maxY, imageItem.y + imageItem.height);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加一些边距
|
|
|
+ const padding = 20;
|
|
|
+ const newWidth = Math.max(400, maxX - minX + padding * 2);
|
|
|
+ const newHeight = Math.max(300, maxY - minY + padding * 2);
|
|
|
+
|
|
|
+ // 调整所有图片位置
|
|
|
+ const offsetX = padding - minX;
|
|
|
+ const offsetY = padding - minY;
|
|
|
+
|
|
|
+ this.images.forEach(imageItem => {
|
|
|
+ imageItem.x += offsetX;
|
|
|
+ imageItem.y += offsetY;
|
|
|
+ this.updateImagePosition(imageItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新画布大小
|
|
|
+ this.canvasWidth = newWidth;
|
|
|
+ this.canvasHeight = newHeight;
|
|
|
+ this.canvas.style.width = newWidth + 'px';
|
|
|
+ this.canvas.style.height = newHeight + 'px';
|
|
|
+
|
|
|
+ // 更新画布大小输入框
|
|
|
+ document.getElementById('canvasWidth').value = newWidth;
|
|
|
+ document.getElementById('canvasHeight').value = newHeight;
|
|
|
+
|
|
|
+ this.updateInfo();
|
|
|
+ this.setStatus(`画布已调整为 ${newWidth}x${newHeight}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ zoomIn() {
|
|
|
+ this.zoom = Math.min(this.zoom * 1.2, 3);
|
|
|
+ this.applyZoom();
|
|
|
+ }
|
|
|
+
|
|
|
+ zoomOut() {
|
|
|
+ this.zoom = Math.max(this.zoom / 1.2, 0.1);
|
|
|
+ this.applyZoom();
|
|
|
+ }
|
|
|
+
|
|
|
+ resetZoom() {
|
|
|
+ this.zoom = 1;
|
|
|
+ this.applyZoom();
|
|
|
+ }
|
|
|
+
|
|
|
+ applyZoom() {
|
|
|
+ this.canvas.style.transform = `translate(-50%, -50%) scale(${this.zoom})`;
|
|
|
+ this.updateInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ updateInfo() {
|
|
|
+ document.getElementById('canvasSize').textContent = `${this.canvasWidth} x ${this.canvasHeight}`;
|
|
|
+ document.getElementById('imageCount').textContent = this.images.length;
|
|
|
+ document.getElementById('selectedCount').textContent = this.selectedImages.length;
|
|
|
+ document.getElementById('zoomLevel').textContent = Math.round(this.zoom * 100) + '%';
|
|
|
+ }
|
|
|
+
|
|
|
+ setStatus(message) {
|
|
|
+ document.getElementById('statusText').textContent = message;
|
|
|
+ setTimeout(() => {
|
|
|
+ document.getElementById('statusText').textContent = '就绪';
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 全局函数
|
|
|
+ let composer;
|
|
|
+
|
|
|
+ function init() {
|
|
|
+ composer = new ImageComposer();
|
|
|
+ }
|
|
|
+
|
|
|
+ function importImages() {
|
|
|
+ document.getElementById('fileInput').click();
|
|
|
+ }
|
|
|
+
|
|
|
+ function exportCanvas() {
|
|
|
+ composer.exportCanvas();
|
|
|
+ }
|
|
|
+
|
|
|
+ function exportFullCanvas() {
|
|
|
+ composer.exportFullCanvas();
|
|
|
+ }
|
|
|
+
|
|
|
+ function fitCanvasToContent() {
|
|
|
+ composer.fitCanvasToContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearCanvas() {
|
|
|
+ composer.clearCanvas();
|
|
|
+ }
|
|
|
+
|
|
|
+ function selectAll() {
|
|
|
+ composer.selectAll();
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearSelection() {
|
|
|
+ composer.clearSelection();
|
|
|
+ }
|
|
|
+
|
|
|
+ function deleteSelected() {
|
|
|
+ composer.deleteSelected();
|
|
|
+ }
|
|
|
+
|
|
|
+ function alignHorizontal() {
|
|
|
+ composer.alignHorizontal();
|
|
|
+ }
|
|
|
+
|
|
|
+ function alignVertical() {
|
|
|
+ composer.alignVertical();
|
|
|
+ }
|
|
|
+
|
|
|
+ function distributeHorizontal() {
|
|
|
+ composer.distributeHorizontal();
|
|
|
+ }
|
|
|
+
|
|
|
+ function distributeVertical() {
|
|
|
+ composer.distributeVertical();
|
|
|
+ }
|
|
|
+
|
|
|
+ function distributeHorizontalCustom() {
|
|
|
+ composer.distributeHorizontalCustom();
|
|
|
+ }
|
|
|
+
|
|
|
+ function resizeCanvas() {
|
|
|
+ composer.resizeCanvas();
|
|
|
+ }
|
|
|
+
|
|
|
+ function zoomIn() {
|
|
|
+ composer.zoomIn();
|
|
|
+ }
|
|
|
+
|
|
|
+ function zoomOut() {
|
|
|
+ composer.zoomOut();
|
|
|
+ }
|
|
|
+
|
|
|
+ function resetZoom() {
|
|
|
+ composer.resetZoom();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ document.addEventListener('DOMContentLoaded', init);
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|