// 极简邀请函制作工具 (function() { const fileInput = document.getElementById('fileInput'); const addImageBtn = document.getElementById('addImageBtn'); const addOverlayBtn = document.getElementById('addOverlayBtn'); const addTextBtn = document.getElementById('addTextBtn'); const saveBtn = document.getElementById('saveBtn'); const imageContainer = document.getElementById('imageContainer'); const bgLayer = document.getElementById('bgLayer'); const bgImage = document.getElementById('bgImage'); const emptyTip = document.getElementById('emptyTip'); const input1 = document.getElementById('input1'); const input2 = document.getElementById('input2'); const input3 = document.getElementById('input3'); const exportCanvas = document.getElementById('exportCanvas'); const longPressTip = document.getElementById('longPressTip'); const canvasArea = document.getElementById('canvasArea'); const editorToolbar = document.getElementById('editorToolbar'); const textTools = document.getElementById('textTools'); const imageTools = document.getElementById('imageTools'); const bgTools = document.getElementById('bgTools'); const opacitySlider = document.getElementById('opacitySlider'); const opacityValue = document.getElementById('opacityValue'); const bgScaleSlider = document.getElementById('bgScaleSlider'); const bgScaleValue = document.getElementById('bgScaleValue'); const bgOpacitySlider = document.getElementById('bgOpacitySlider'); const bgOpacityValue = document.getElementById('bgOpacityValue'); const boldBtn = document.getElementById('boldBtn'); const italicBtn = document.getElementById('italicBtn'); const colorPicker = document.getElementById('colorPicker'); const fontPicker = document.getElementById('fontPicker'); const fontSizePicker = document.getElementById('fontSizePicker'); const overlayLayer = document.getElementById('overlayLayer'); const overlayInput = document.createElement('input'); overlayInput.type = 'file'; overlayInput.accept = 'image/*'; overlayInput.hidden = true; document.body.appendChild(overlayInput); const guideH = document.getElementById('guideH'); const guideV = document.getElementById('guideV'); const gridLines = document.getElementById('gridLines'); let currentImage = null; let bgImageScale = 1; let bgImageOpacity = 1; let selectedInput = null; let editingInput = null; let longPressTimer = null; // 图片滑动调整位置相关变量 let imageOffsetX = 0; let isDraggingImage = false; let imageStartX = 0; let imageStartOffsetX = 0; let minOffsetX = 0; let maxOffsetX = 0; let isInteractingWithBgImage = false; // 初始化 init(); function init() { // 添加图片按钮 addImageBtn.addEventListener('click', () => { fileInput.click(); }); // 层叠图按钮 addOverlayBtn.addEventListener('click', () => { overlayInput.click(); }); // 层叠图文件选择 overlayInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { loadOverlayImage(file); } overlayInput.value = ''; }); // 添加文本按钮 addTextBtn.addEventListener('click', () => { addTextInput(); }); // 文件选择 fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { loadImage(file); } fileInput.value = ''; }); // 保存按钮 saveBtn.addEventListener('click', saveImage); // 初始化输入框 initInput(input1); initInput(input2); initInput(input3); // 为三个输入框添加删除按钮 [input1, input2, input3].forEach(inputEl => { const deleteBtn = document.createElement('button'); deleteBtn.className = 'text-delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); inputEl.remove(); }); inputEl.appendChild(deleteBtn); }); // 初始化图片滑动 initImageDrag(); // 透明度滑块 opacitySlider.addEventListener('input', (e) => { if (selectedOverlay) { selectedOverlay.style.opacity = e.target.value / 100; opacityValue.textContent = e.target.value + '%'; } }); // 背景图缩放滑块 bgScaleSlider.addEventListener('input', (e) => { bgImageScale = e.target.value / 100; bgScaleValue.textContent = e.target.value + '%'; updateBgImage(); }); // 背景图透明度滑块 bgOpacitySlider.addEventListener('input', (e) => { bgImageOpacity = e.target.value / 100; bgOpacityValue.textContent = e.target.value + '%'; bgImage.style.opacity = bgImageOpacity; }); // 点击空白区域取消选中 document.addEventListener('click', (e) => { // 只有点击真正的空白区域(不是输入框,不是图片容器,不是工具栏)才取消选中 const isTextInput = e.target.closest('.text-input'); const isImageContainer = e.target.closest('.image-container'); const isToolbar = e.target.closest('.toolbar'); const isEditorToolbar = e.target.closest('.editor-toolbar'); if (!isTextInput && !isImageContainer && !isToolbar && !isEditorToolbar) { deselectAll(); } }); // 点击容器空白处取消选中 function handleContainerClick(e) { if (isInteractingWithBgImage) { return; } const isBackground = e.target === imageContainer || e.target === bgLayer || e.target === bgImage || e.target.closest('.preview-bg-layer') || e.target === document.getElementById('emptyTip'); const isOverlayImage = e.target.closest('.overlay-image'); const isTextInput = e.target.closest('.text-input'); // 点击背景/空白区域 if (isBackground && !isOverlayImage) { // 如果正在编辑,先结束编辑 if (editingInput) { endEdit(editingInput); } // 移除输入框选中状态 if (selectedInput) { selectedInput.classList.remove('selected'); selectedInput = null; } // 取消层叠图选中状态 if (selectedOverlay) { selectedOverlay.classList.remove('selected'); selectedOverlay = null; } // 隐藏辅助线 hideGuides(); // 隐藏编辑器工具栏 editorToolbar.classList.remove('active'); } // 点击空白区域(不是层叠图和输入框) if (!isOverlayImage && !isTextInput && !isBackground) { if (selectedOverlay) { selectedOverlay.classList.remove('selected'); selectedOverlay = null; } } } imageContainer.addEventListener('click', handleContainerClick); // 移动端:touchend 时如果正在编辑文本,不处理容器点击 imageContainer.addEventListener('touchend', (e) => { // 如果触摸目标是文本输入框或正在编辑,不触发容器点击 if (e.target.closest('.text-input')) { return; } // 如果正在编辑文本,不触发容器点击(避免隐藏工具栏) if (editingInput) { return; } handleContainerClick(e); }); // 初始化长按保存(只在3:5预览区域) initLongPressSave(); // 初始化右键菜单(只在3:5预览区域) initContextMenu(); // 初始化文本编辑器工具栏 initTextEditorToolbar(); // 初始化键盘微调功能 initKeyboardNudge(); // 加载默认背景图 loadDefaultBackground(); // 显示长按提示 setTimeout(() => { longPressTip.classList.add('show'); setTimeout(() => { longPressTip.classList.remove('show'); }, 3000); }, 1000); } // 加载默认背景图 function loadDefaultBackground() { const defaultBgUrl = './images/default-bg.jpg'; const img = new Image(); img.onload = () => { currentImage = img; bgImage.src = img.src; imageContainer.classList.remove('empty'); emptyTip.classList.add('hidden'); // 计算图片尺寸和边界 calculateImageBounds(); // 背景图选择后默认居中显示 imageOffsetX = 0; // 重置缩放 bgImageScale = 1; bgScaleSlider.value = 100; bgScaleValue.textContent = '100%'; // 重置透明度 bgImageOpacity = 1; bgOpacitySlider.value = 100; bgOpacityValue.textContent = '100%'; updateBgImage(); // 显示预设的文本输入框 showPresetTextInputs(); }; img.onerror = () => { console.log('默认背景图加载失败,使用空状态'); }; img.src = defaultBgUrl; } // 加载图片 function loadImage(file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { currentImage = img; bgImage.src = img.src; imageContainer.classList.remove('empty'); emptyTip.classList.add('hidden'); // 计算图片尺寸和边界 calculateImageBounds(); // 背景图选择后默认居中显示 imageOffsetX = 0; // 重置缩放 bgImageScale = 1; bgScaleSlider.value = 100; bgScaleValue.textContent = '100%'; // 重置透明度 bgImageOpacity = 1; bgOpacitySlider.value = 100; bgOpacityValue.textContent = '100%'; updateBgImage(); // 显示预设的文本输入框 showPresetTextInputs(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } // 显示预设的文本输入框 function showPresetTextInputs() { [input1, input2, input3].forEach(inputEl => { if (inputEl) { inputEl.classList.remove('hidden'); } }); } // 添加文本输入框 function addTextInput() { const container = imageContainer; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const textEl = document.createElement('div'); textEl.className = 'text-input'; textEl.contentEditable = 'false'; textEl.textContent = '新文本'; const left = Math.random() * (containerWidth * 0.6) + containerWidth * 0.1; const top = Math.random() * (containerHeight * 0.6) + containerHeight * 0.1; textEl.style.left = (left / containerWidth * 100) + '%'; textEl.style.top = (top / containerHeight * 100) + '%'; // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'text-delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); textEl.remove(); }); textEl.appendChild(deleteBtn); const inputLayer = container.querySelector('.preview-input-layer'); inputLayer.appendChild(textEl); initInput(textEl); selectInput(textEl); } // 加载层叠图 function loadOverlayImage(file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { createOverlayElement(img); }; img.src = e.target.result; }; reader.readAsDataURL(file); } // 创建层叠图元素 function createOverlayElement(img) { const container = imageContainer; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // 计算初始尺寸(不超过容器1/3) let width = img.naturalWidth; let height = img.naturalHeight; const maxSize = Math.min(containerWidth, containerHeight) / 2; if (width > maxSize || height > maxSize) { if (width > height) { height = (height / width) * maxSize; width = maxSize; } else { width = (width / height) * maxSize; height = maxSize; } } // 创建层叠图元素 const overlayEl = document.createElement('div'); overlayEl.className = 'overlay-image'; overlayEl.dataset.scale = 1; const imgEl = document.createElement('img'); imgEl.src = img.src; imgEl.style.width = '100%'; imgEl.style.height = '100%'; imgEl.style.objectFit = 'contain'; overlayEl.appendChild(imgEl); // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); overlayEl.remove(); }); overlayEl.appendChild(deleteBtn); // 居中显示 overlayEl.style.left = (containerWidth / 2 - width / 2) + 'px'; overlayEl.style.top = (containerHeight / 2 - height / 2) + 'px'; overlayEl.style.width = width + 'px'; overlayEl.style.height = height + 'px'; // 初始化拖拽和缩放 initOverlayImage(overlayEl); overlayLayer.appendChild(overlayEl); // 选中新添加的层叠图 selectOverlay(overlayEl); } // 初始化层叠图交互 function initOverlayImage(overlayEl) { let isDragging = false; let isResizing = false; let startX, startY, startLeft, startTop, startWidth, startHeight; const minSize = 30; // 点击选中(区分点击和拖拽) overlayEl.addEventListener('click', (e) => { e.stopPropagation(); selectOverlay(overlayEl); }); // 拖拽移动(不选中也可拖动) overlayEl.addEventListener('mousedown', handleDragStart); overlayEl.addEventListener('touchstart', handleDragStart, { passive: false }); function handleDragStart(e) { if (e.target.classList.contains('delete-btn') || e.target.classList.contains('resize-handle')) return; e.stopPropagation(); // 阻止事件冒泡到 imageContainer const isTouch = e.type === 'touchstart'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; isDragging = true; startX = clientX; startY = clientY; startLeft = overlayEl.offsetLeft; startTop = overlayEl.offsetTop; overlayEl.classList.add('dragging'); // 拖动时设置为选中状态 selectOverlay(overlayEl); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); document.addEventListener('touchmove', handleDragMove, { passive: false }); document.addEventListener('touchend', handleDragEnd); } function handleDragMove(e) { if (!isDragging) return; const isTouch = e.type === 'touchmove'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; const dx = clientX - startX; const dy = clientY - startY; // 移除移动距离限制,实现 1px 精确移动 let newLeft = startLeft + dx; let newTop = startTop + dy; // 不启用吸附,只限制边界,但显示辅助线 const snapped = getSnapPosition(overlayEl, newLeft, newTop, false); newLeft = snapped.left; newTop = snapped.top; // 显示对齐辅助线 showAlignmentGuides(overlayEl); overlayEl.style.left = newLeft + 'px'; overlayEl.style.top = newTop + 'px'; e.preventDefault(); } function handleDragEnd() { isDragging = false; overlayEl.classList.remove('dragging'); hideGuides(); document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); document.removeEventListener('touchmove', handleDragMove); document.removeEventListener('touchend', handleDragEnd); } // 缩放控制(四角) const corners = ['tl', 'tr', 'bl', 'br']; corners.forEach(corner => { const handle = document.createElement('div'); handle.className = 'resize-handle resize-' + corner; handle.style.cssText = ` position: absolute; width: 12px; height: 12px; background: #4a90e2; border: 1px solid #fff; `; switch(corner) { case 'tl': handle.style.top = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'nw-resize'; break; case 'tr': handle.style.top = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'ne-resize'; break; case 'bl': handle.style.bottom = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'sw-resize'; break; case 'br': handle.style.bottom = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'se-resize'; break; } handle.addEventListener('mousedown', startResize); handle.addEventListener('touchstart', startResize, { passive: false }); overlayEl.appendChild(handle); function startResize(e) { e.stopPropagation(); isResizing = true; const isTouch = e.type === 'touchstart'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; startX = clientX; startY = clientY; startLeft = overlayEl.offsetLeft; startTop = overlayEl.offsetTop; startWidth = overlayEl.offsetWidth; startHeight = overlayEl.offsetHeight; document.addEventListener('mousemove', handleResize); document.addEventListener('mouseup', endResize); document.addEventListener('touchmove', handleResize, { passive: false }); document.addEventListener('touchend', endResize); e.preventDefault(); } function handleResize(e) { if (!isResizing) return; const isTouch = e.type === 'touchmove'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; const dx = clientX - startX; const dy = clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; const aspectRatio = startWidth / startHeight; // 根据角落调整尺寸 if (corner === 'br') { newWidth = Math.max(minSize, startWidth + dx); newHeight = newWidth / aspectRatio; } else if (corner === 'bl') { newWidth = Math.max(minSize, startWidth - dx); newHeight = newWidth / aspectRatio; newLeft = startLeft - (newWidth - startWidth); } else if (corner === 'tr') { newWidth = Math.max(minSize, startWidth + dx); newHeight = newWidth / aspectRatio; newTop = startTop - (newHeight - startHeight); } else if (corner === 'tl') { newWidth = Math.max(minSize, startWidth - dx); newHeight = newWidth / aspectRatio; newLeft = startLeft - (newWidth - startWidth); newTop = startTop - (newHeight - startHeight); } overlayEl.style.width = newWidth + 'px'; overlayEl.style.height = newHeight + 'px'; overlayEl.style.left = newLeft + 'px'; overlayEl.style.top = newTop + 'px'; e.preventDefault(); } function endResize() { isResizing = false; document.removeEventListener('mousemove', handleResize); document.removeEventListener('mouseup', endResize); document.removeEventListener('touchmove', handleResize); document.removeEventListener('touchend', endResize); } }); } // 选中层叠图(同时取消输入框选中) let selectedOverlay = null; function selectOverlay(overlayEl) { if (selectedInput) { selectedInput.classList.remove('selected'); selectedInput = null; } if (editingInput) { endEdit(editingInput); } if (selectedOverlay && selectedOverlay !== overlayEl) { selectedOverlay.classList.remove('selected'); } selectedOverlay = overlayEl; overlayEl.classList.add('selected'); // 显示编辑器工具栏和图片工具 editorToolbar.classList.add('active'); textTools.classList.remove('active'); imageTools.classList.add('active'); bgTools.classList.remove('active'); // 更新透明度滑块为当前层叠图的透明度 const currentOpacity = overlayEl.style.opacity || '1'; opacitySlider.value = parseFloat(currentOpacity) * 100; opacityValue.textContent = opacitySlider.value + '%'; } // 计算图片滑动边界 function calculateImageBounds() { if (!currentImage) return; const containerWidth = imageContainer.clientWidth; const containerHeight = imageContainer.clientHeight; const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight; const containerRatio = containerWidth / containerHeight; let imgWidth, imgHeight; if (imgRatio > containerRatio) { // 图片更宽,以高度为基准计算宽度 imgHeight = containerHeight; imgWidth = imgHeight * imgRatio; } else { // 图片更高或等比,以宽度为基准 imgWidth = containerWidth; imgHeight = imgWidth / imgRatio; } // 计算滑动边界:严格以图片边缘为边界,不能露出白板 // 滑动公式:imageOffsetX = imageStartOffsetX + deltaX // 手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧 // 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧 // 初始位置居中:imageOffsetX = 0,此时 baseLeft 已使图片居中 if (imgWidth > containerWidth) { // 图片宽度大于容器,可以滑动 // minOffsetX = -(imgWidth - containerWidth) / 2:向左滑到极限(显示左侧) // maxOffsetX = (imgWidth - containerWidth) / 2:向右滑到极限(显示右侧) const halfRange = (imgWidth - containerWidth) / 2; minOffsetX = -halfRange; maxOffsetX = halfRange; } else { // 图片宽度小于等于容器,居中显示,不能滑动 minOffsetX = 0; maxOffsetX = 0; } } // 更新背景图(缩放和透明度) function updateBgImage() { if (!currentImage) return; const containerWidth = imageContainer.clientWidth; const containerHeight = imageContainer.clientHeight; const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight; const containerRatio = containerWidth / containerHeight; let imgWidth, imgHeight; if (imgRatio > containerRatio) { imgHeight = containerHeight; imgWidth = imgHeight * imgRatio; } else { imgWidth = containerWidth; imgHeight = imgWidth / imgRatio; } // 应用缩放 bgImage.style.width = (imgWidth * bgImageScale) + 'px'; bgImage.style.height = (imgHeight * bgImageScale) + 'px'; // 应用透明度 bgImage.style.opacity = bgImageOpacity; // 重新计算边界 const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; if (scaledWidth > containerWidth) { minOffsetX = -(scaledWidth - containerWidth) / 2; maxOffsetX = (scaledWidth - containerWidth) / 2; } else { minOffsetX = 0; maxOffsetX = 0; } // 限制当前位置在新范围内 imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX)); // 重新应用位置(居中 + 偏移) const baseLeft = (containerWidth - scaledWidth) / 2; bgImage.style.left = (baseLeft + imageOffsetX) + 'px'; bgImage.style.top = ((containerHeight - scaledHeight) / 2) + 'px'; } // 更新图片位置 function updateImagePosition() { if (!currentImage) return; const containerWidth = imageContainer.clientWidth; const containerHeight = imageContainer.clientHeight; const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight; let imgWidth, imgHeight; if (imgRatio > containerWidth / containerHeight) { imgHeight = containerHeight; imgWidth = imgHeight * imgRatio; } else { imgWidth = containerWidth; imgHeight = imgWidth / imgRatio; } // 应用缩放 const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; // 限制滑动范围 if (scaledWidth > containerWidth) { minOffsetX = -(scaledWidth - containerWidth) / 2; maxOffsetX = (scaledWidth - containerWidth) / 2; } else { minOffsetX = 0; maxOffsetX = 0; } imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX)); // 应用位置 const baseLeft = (containerWidth - scaledWidth) / 2; bgImage.style.width = scaledWidth + 'px'; bgImage.style.height = scaledHeight + 'px'; bgImage.style.left = (baseLeft + imageOffsetX) + 'px'; bgImage.style.top = ((containerHeight - scaledHeight) / 2) + 'px'; bgImage.style.opacity = bgImageOpacity; bgImage.style.transform = 'none'; } // 初始化图片滑动调整位置 function initImageDrag() { // 鼠标按下 imageContainer.addEventListener('mousedown', handleImageStart); imageContainer.addEventListener('touchstart', handleImageStart, { passive: false }); // 鼠标移动 document.addEventListener('mousemove', handleImageMove); document.addEventListener('touchmove', handleImageMove, { passive: false }); // 鼠标释放 document.addEventListener('mouseup', handleImageEnd); document.addEventListener('touchend', handleImageEnd); // 点击背景图显示工具栏 bgImage.addEventListener('click', (e) => { if (!currentImage) return; e.stopPropagation(); isInteractingWithBgImage = true; // 显示背景图工具栏 editorToolbar.classList.add('active'); textTools.classList.remove('active'); imageTools.classList.remove('active'); bgTools.classList.add('active'); // 同步滑块值 bgScaleSlider.value = bgImageScale * 100; bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; bgOpacitySlider.value = bgImageOpacity * 100; bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%'; setTimeout(() => { isInteractingWithBgImage = false; }, 100); }); bgLayer.addEventListener('click', (e) => { if (!currentImage) return; e.stopPropagation(); isInteractingWithBgImage = true; // 显示背景图工具栏 editorToolbar.classList.add('active'); textTools.classList.remove('active'); imageTools.classList.remove('active'); bgTools.classList.add('active'); // 同步滑块值 bgScaleSlider.value = bgImageScale * 100; bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; bgOpacitySlider.value = bgImageOpacity * 100; bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%'; setTimeout(() => { isInteractingWithBgImage = false; }, 100); }); function handleImageStart(e) { // 如果点击的是输入框,不处理图片滑动 if (e.target.closest('.text-input')) { return; } // 如果正在编辑,先退出编辑状态 if (editingInput) { deselectAll(); } // 如果没有图片,不处理 if (!currentImage) { return; } const isTouch = e.type === 'touchstart'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; // 只有点击图片本身或背景图层时才启动图片滑动 const isImageOrBgLayer = e.target === bgImage || e.target === bgLayer || e.target === imageContainer; if (!isImageOrBgLayer) { return; } isDraggingImage = true; isInteractingWithBgImage = true; imageStartX = clientX; imageStartOffsetX = imageOffsetX; // 显示背景图工具栏 editorToolbar.classList.add('active'); textTools.classList.remove('active'); imageTools.classList.remove('active'); bgTools.classList.add('active'); // 同步滑块值 bgScaleSlider.value = bgImageScale * 100; bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; bgOpacitySlider.value = bgImageOpacity * 100; bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%'; if (isTouch) { e.preventDefault(); } } function handleImageMove(e) { if (!isDraggingImage) return; const isTouch = e.type === 'touchmove'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const deltaX = clientX - imageStartX; // 反转拖拽:手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧 // 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧 imageOffsetX = imageStartOffsetX + deltaX; updateImagePosition(); if (isTouch) { e.preventDefault(); } } function handleImageEnd() { isDraggingImage = false; isInteractingWithBgImage = false; } } // 初始化输入框 - 每个输入框独立管理自己的拖拽状态 function initInput(element) { let isMouseDown = false; let isDragging = false; let dragTimer = null; let startX, startY, startLeft, startTop; let clickStartTime = 0; let clickStartX = 0; let clickStartY = 0; let hasMoved = false; // 检查是否需要显示 placeholder function updatePlaceholder() { // 获取纯文本内容(排除删除按钮) const textContent = Array.from(element.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('text-delete-btn'))) .map(node => node.textContent) .join('') .trim(); if (!textContent) { element.classList.add('show-placeholder'); } else { element.classList.remove('show-placeholder'); } } // 初始化时检查 updatePlaceholder(); // 监听输入事件,控制 placeholder 显示 element.addEventListener('input', updatePlaceholder); // 鼠标/触摸按下 element.addEventListener('mousedown', handleStart); element.addEventListener('touchstart', handleStart, { passive: false }); function handleStart(e) { const isTouch = e.type === 'touchstart'; // 如果正在编辑此输入框,不处理拖拽事件,让浏览器处理文本选择 if (editingInput === element) { return; } const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; isMouseDown = true; clickStartTime = Date.now(); clickStartX = clientX; clickStartY = clientY; hasMoved = false; isDragging = false; // 选中此输入框 selectInput(element); startX = clientX; startY = clientY; const containerRect = imageContainer.getBoundingClientRect(); const elemRect = element.getBoundingClientRect(); startLeft = elemRect.left - containerRect.left; startTop = elemRect.top - containerRect.top; // 延迟判断是否为拖拽 dragTimer = setTimeout(() => { if (isMouseDown) { isDragging = true; element.classList.add('dragging'); } }, 150); // 阻止事件冒泡,避免触发图片滑动 e.stopPropagation(); } // 鼠标/触摸移动 element.addEventListener('mousemove', handleMove); element.addEventListener('touchmove', handleMove, { passive: false }); function handleMove(e) { // 如果正在编辑,不拦截事件,让浏览器处理文本选择 if (editingInput === element) { return; } if (!isMouseDown) return; if (!selectedInput) return; const isTouch = e.type === 'touchmove'; const clientX = isTouch ? e.touches[0].clientX : e.clientX; const clientY = isTouch ? e.touches[0].clientY : e.clientY; const moveDistance = Math.sqrt( Math.pow(clientX - clickStartX, 2) + Math.pow(clientY - clickStartY, 2) ); // 只要有移动就开始拖拽,实现 1px 精确移动 if (moveDistance > 0) { hasMoved = true; if (dragTimer) { clearTimeout(dragTimer); dragTimer = null; } if (!isDragging) { isDragging = true; element.classList.add('dragging'); } } if (isDragging) { const dx = clientX - startX; const dy = clientY - startY; const newLeft = startLeft + dx; const newTop = startTop + dy; // 不启用吸附,只限制边界,但显示辅助线 const snapped = getSnapPosition(element, newLeft, newTop, false); element.style.left = snapped.left + 'px'; element.style.top = snapped.top + 'px'; // 显示对齐辅助线 showAlignmentGuides(element); } // 阻止事件冒泡,避免触发图片滑动 e.stopPropagation(); } // 鼠标/触摸释放 element.addEventListener('mouseup', handleEnd); element.addEventListener('touchend', handleEnd); element.addEventListener('mouseleave', (e) => { if (isMouseDown) { handleEnd(e); } }); function handleEnd(e) { isMouseDown = false; if (dragTimer) { clearTimeout(dragTimer); dragTimer = null; } if (isDragging) { isDragging = false; element.classList.remove('dragging'); hideGuides(); } // 阻止事件冒泡 if (e) { e.stopPropagation(); } } // 双击进入编辑模式(电脑端) element.addEventListener('dblclick', (e) => { e.stopPropagation(); startEdit(element); element.dataset.justEdited = 'true'; setTimeout(() => { element.dataset.justEdited = 'false'; }, 200); }); // 单击进入编辑模式(同时支持桌面和移动端) element.addEventListener('click', (e) => { if (hasMoved) return; if (element.dataset.justEdited === 'true') return; // 如果已经在编辑中,不重复进入 if (editingInput === element) return; // 选中并立即进入编辑模式 selectInput(element); startEdit(element); }); } // 选中输入框 function selectInput(element) { if (selectedOverlay) { selectedOverlay.classList.remove('selected'); selectedOverlay = null; } if (editingInput && editingInput !== element) { endEdit(editingInput); } if (selectedInput && selectedInput !== element) { selectedInput.classList.remove('selected'); } selectedInput = element; element.classList.add('selected'); element.classList.remove('editing'); element.contentEditable = 'false'; // 显示编辑器工具栏和文本工具 editorToolbar.classList.add('active'); textTools.classList.add('active'); imageTools.classList.remove('active'); bgTools.classList.remove('active'); // 更新工具栏状态 updateToolbarState(element); } // 开始编辑 function startEdit(element) { if (editingInput && editingInput !== element) { endEdit(editingInput); } editingInput = element; selectedInput = element; element.classList.add('editing'); element.classList.remove('selected'); element.contentEditable = 'true'; // 显示编辑器工具栏和文本工具 editorToolbar.classList.add('active'); textTools.classList.add('active'); imageTools.classList.remove('active'); bgTools.classList.remove('active'); // 移动端/编辑模式:固定在顶部始终可见 if (window.innerWidth <= 768 || 'ontouchstart' in window) { editorToolbar.classList.add('editing-mode'); // 滚动使工具栏和编辑元素可见 setTimeout(() => { const rect = element.getBoundingClientRect(); const toolbarHeight = editorToolbar.offsetHeight || 50; // 如果文本被工具栏遮挡,滚动页面 if (rect.top < toolbarHeight + 10) { window.scrollBy({ top: rect.top - toolbarHeight - 20, behavior: 'smooth' }); } }, 300); // 等待键盘弹出 } // 更新工具栏状态 updateToolbarState(element); // 立即 focus 以触发键盘(必须在用户交互的同步上下文中) element.focus(); // 如果没有选中文本,则定位光标到末尾 setTimeout(() => { const sel = window.getSelection(); if (sel.isCollapsed) { const range = document.createRange(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } }, 0); // 使用 Virtual Keyboard API(如果支持) if ('virtualKeyboard' in navigator) { navigator.virtualKeyboard.show(); } } // 结束编辑 function endEdit(element) { element.classList.remove('editing'); element.classList.add('selected'); element.contentEditable = 'false'; // 移除移动端编辑模式样式 editorToolbar.classList.remove('editing-mode'); // 检查是否需要显示 placeholder const textContent = Array.from(element.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('text-delete-btn'))) .map(node => node.textContent) .join('') .trim(); if (!textContent) { element.classList.add('show-placeholder'); } else { element.classList.remove('show-placeholder'); } element.blur(); editingInput = null; // 隐藏编辑器工具栏 editorToolbar.classList.remove('active'); } // 更新工具栏状态 function updateToolbarState(element) { const style = window.getComputedStyle(element); const fontWeight = style.fontWeight; const fontStyle = style.fontStyle; const color = style.color; const fontFamily = style.fontFamily.replace(/['"]/g, ''); const fontSize = parseInt(style.fontSize); // 更新加粗按钮状态 boldBtn.classList.toggle('active', fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 700); // 更新斜体按钮状态 italicBtn.classList.toggle('active', fontStyle === 'italic'); // 更新颜色选择器 colorPicker.value = rgbToHex(color); // 更新字体选择器 - 匹配字体栈中的主要字体 let matched = false; for (let option of fontPicker.options) { const optionFonts = option.value.toLowerCase().split(',').map(f => f.trim()); const currentFonts = fontFamily.toLowerCase().split(',').map(f => f.trim()); // 检查是否有主要字体匹配 if (optionFonts.some(of => currentFonts.includes(of))) { fontPicker.value = option.value; matched = true; break; } } // 如果没有匹配,默认选择第一个(系统默认) if (!matched && fontPicker.options.length > 0) { fontPicker.value = fontPicker.options[0].value; } // 更新字号选择器 const fontSizeValueEl = fontSizePicker.querySelector('.roller-value'); if (fontSizeValueEl) { const displaySize = isNaN(fontSize) ? 16 : fontSize; fontSizeValueEl.textContent = displaySize.toString(); fontSizeValue = displaySize; } } // 获取吸附后的位置(可选吸附,不强制) function getSnapPosition(draggedEl, newLeft, newTop, enableSnap = false) { const container = imageContainer; const containerRect = container.getBoundingClientRect(); const elWidth = draggedEl.offsetWidth; const elHeight = draggedEl.offsetHeight; // 计算元素各参考点相对容器的位置 const elCenterX = newLeft + elWidth / 2; const elCenterY = newTop + elHeight / 2; const elLeft = newLeft; const elRight = newLeft + elWidth; const elTop = newTop; const elBottom = newTop + elHeight; // 容器尺寸 const cWidth = containerRect.width; const cHeight = containerRect.height; const cCenterX = cWidth / 2; const cCenterY = cHeight / 2; // 默认返回原始位置(不吸附) let snapLeft = newLeft; let snapTop = newTop; let showGuide = false; // 如果不启用吸附,只限制边界并返回 if (!enableSnap) { // 边界限制 snapLeft = Math.max(0, Math.min(snapLeft, cWidth - elWidth)); snapTop = Math.max(0, Math.min(snapTop, cHeight - elHeight)); return { left: snapLeft, top: snapTop, showGuide }; } const threshold = 5; // 显示网格线 if (gridLines) { gridLines.classList.add('visible'); } // 水平垂直分别检测 let snapX = false; let snapY = false; let snapXPos = null; let snapYPos = null; // 检测容器中心对齐 if (Math.abs(elCenterX - cCenterX) < threshold) { snapX = true; snapXPos = cCenterX - elWidth / 2; } if (Math.abs(elCenterY - cCenterY) < threshold) { snapY = true; snapYPos = cCenterY - elHeight / 2; } // 检测容器边缘对齐 if (Math.abs(elLeft) < threshold) { snapX = true; snapXPos = 0; } if (Math.abs(elRight - cWidth) < threshold) { snapX = true; snapXPos = cWidth; } if (Math.abs(elTop) < threshold) { snapY = true; snapYPos = 0; } if (Math.abs(elBottom - cHeight) < threshold) { snapY = true; snapYPos = cHeight; } // 检测其他元素对齐(输入框和层叠图) const otherInputs = [input1, input2, input3].filter(el => el && el !== draggedEl); const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image')) .filter(el => el !== draggedEl); for (const otherEl of otherInputs) { if (!otherEl) continue; const oLeft = otherEl.offsetLeft; const oTop = otherEl.offsetTop; const oWidth = otherEl.offsetWidth; const oHeight = otherEl.offsetHeight; const oRight = oLeft + oWidth; const oBottom = oTop + oHeight; const oCenterX = oLeft + oWidth / 2; const oCenterY = oTop + oHeight / 2; // 垂直边缘对齐 if (Math.abs(elLeft - oLeft) < threshold) { snapX = true; snapXPos = oLeft; } if (Math.abs(elRight - oRight) < threshold) { snapX = true; snapXPos = oRight - elWidth; } if (Math.abs(elLeft - oRight) < threshold) { snapX = true; snapXPos = oRight; } if (Math.abs(elRight - oLeft) < threshold) { snapX = true; snapXPos = oLeft - elWidth; } // 水平边缘对齐 if (Math.abs(elTop - oTop) < threshold) { snapY = true; snapYPos = oTop; } if (Math.abs(elBottom - oBottom) < threshold) { snapY = true; snapYPos = oBottom - elHeight; } if (Math.abs(elTop - oBottom) < threshold) { snapY = true; snapYPos = oBottom; } if (Math.abs(elBottom - oTop) < threshold) { snapY = true; snapYPos = oTop - elHeight; } // 中心对齐 if (Math.abs(elCenterX - oCenterX) < threshold) { snapX = true; snapXPos = oCenterX - elWidth / 2; } if (Math.abs(elCenterY - oCenterY) < threshold) { snapY = true; snapYPos = oCenterY - elHeight / 2; } } // 检测层叠图之间对齐 for (const otherEl of otherOverlays) { if (!otherEl) continue; const oLeft = otherEl.offsetLeft; const oTop = otherEl.offsetTop; const oWidth = otherEl.offsetWidth; const oHeight = otherEl.offsetHeight; const oRight = oLeft + oWidth; const oBottom = oTop + oHeight; const oCenterX = oLeft + oWidth / 2; const oCenterY = oTop + oHeight / 2; // 垂直边缘对齐 if (Math.abs(elLeft - oLeft) < threshold) { snapX = true; snapXPos = oLeft; } if (Math.abs(elRight - oRight) < threshold) { snapX = true; snapXPos = oRight - elWidth; } if (Math.abs(elLeft - oRight) < threshold) { snapX = true; snapXPos = oRight; } if (Math.abs(elRight - oLeft) < threshold) { snapX = true; snapXPos = oLeft - elWidth; } // 水平边缘对齐 if (Math.abs(elTop - oTop) < threshold) { snapY = true; snapYPos = oTop; } if (Math.abs(elBottom - oBottom) < threshold) { snapY = true; snapYPos = oBottom - elHeight; } if (Math.abs(elTop - oBottom) < threshold) { snapY = true; snapYPos = oBottom; } if (Math.abs(elBottom - oTop) < threshold) { snapY = true; snapYPos = oTop - elHeight; } // 中心对齐 if (Math.abs(elCenterX - oCenterX) < threshold) { snapX = true; snapXPos = oCenterX - elWidth / 2; } if (Math.abs(elCenterY - oCenterY) < threshold) { snapY = true; snapYPos = oCenterY - elHeight / 2; } } // 应用吸附(仅在启用吸附时) if (snapX && snapXPos !== null) { snapLeft = snapXPos; showGuide = true; } if (snapY && snapYPos !== null) { snapTop = snapYPos; showGuide = true; } // 边界限制 snapLeft = Math.max(0, Math.min(snapLeft, cWidth - elWidth)); snapTop = Math.max(0, Math.min(snapTop, cHeight - elHeight)); return { left: snapLeft, top: snapTop, showGuide }; } // RGB转十六进制 function rgbToHex(rgb) { if (rgb.startsWith('#')) return rgb; const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); if (!match) return '#000000'; return '#' + [match[1], match[2], match[3]].map(x => parseInt(x).toString(16).padStart(2, '0')).join(''); } // 对齐辅助线 - 水平和垂直居中、对齐其他元素 function showAlignmentGuides(draggedEl) { const container = imageContainer; const cWidth = container.offsetWidth; const cHeight = container.offsetHeight; const elWidth = draggedEl.offsetWidth; const elHeight = draggedEl.offsetHeight; const elLeft = draggedEl.offsetLeft; const elTop = draggedEl.offsetTop; const elRight = elLeft + elWidth; const elBottom = elTop + elHeight; const elCenterX = elLeft + elWidth / 2; const elCenterY = elTop + elHeight / 2; const threshold = 1; let guideHPos = null; let guideVPos = null; // 容器中心对齐 const cCenterX = cWidth / 2; const cCenterY = cHeight / 2; if (Math.abs(elCenterX - cCenterX) < threshold) { guideVPos = cCenterX; } if (Math.abs(elCenterY - cCenterY) < threshold) { guideHPos = cCenterY; } // 容器边缘对齐 if (Math.abs(elLeft) < threshold) { guideVPos = 0; } if (Math.abs(elRight - cWidth) < threshold) { guideVPos = cWidth; } if (Math.abs(elTop) < threshold) { guideHPos = 0; } if (Math.abs(elBottom - cHeight) < threshold) { guideHPos = cHeight; } // 其他输入框对齐检测 const otherInputs = [input1, input2].filter(el => el && el !== draggedEl); const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image')) .filter(el => el !== draggedEl); for (const otherEl of otherInputs) { if (!otherEl) continue; const oLeft = otherEl.offsetLeft; const oTop = otherEl.offsetTop; const oWidth = otherEl.offsetWidth; const oHeight = otherEl.offsetHeight; const oRight = oLeft + oWidth; const oBottom = oTop + oHeight; const oCenterX = oLeft + oWidth / 2; const oCenterY = oTop + oHeight / 2; // 垂直边缘对齐 if (Math.abs(elLeft - oLeft) < threshold) { guideVPos = oLeft; } if (Math.abs(elRight - oRight) < threshold) { guideVPos = oRight; } if (Math.abs(elLeft - oRight) < threshold) { guideVPos = oRight; } if (Math.abs(elRight - oLeft) < threshold) { guideVPos = oLeft; } // 水平边缘对齐 if (Math.abs(elTop - oTop) < threshold) { guideHPos = oTop; } if (Math.abs(elBottom - oBottom) < threshold) { guideHPos = oBottom; } if (Math.abs(elTop - oBottom) < threshold) { guideHPos = oBottom; } if (Math.abs(elBottom - oTop) < threshold) { guideHPos = oTop; } // 中心对齐 if (Math.abs(elCenterX - oCenterX) < threshold) { guideVPos = oCenterX; } if (Math.abs(elCenterY - oCenterY) < threshold) { guideHPos = oCenterY; } } // 其他层叠图对齐检测 for (const otherEl of otherOverlays) { if (!otherEl) continue; const oLeft = otherEl.offsetLeft; const oTop = otherEl.offsetTop; const oWidth = otherEl.offsetWidth; const oHeight = otherEl.offsetHeight; const oRight = oLeft + oWidth; const oBottom = oTop + oHeight; const oCenterX = oLeft + oWidth / 2; const oCenterY = oTop + oHeight / 2; // 垂直边缘对齐 if (Math.abs(elLeft - oLeft) < threshold) { guideVPos = oLeft; } if (Math.abs(elRight - oRight) < threshold) { guideVPos = oRight; } if (Math.abs(elLeft - oRight) < threshold) { guideVPos = oRight; } if (Math.abs(elRight - oLeft) < threshold) { guideVPos = oLeft; } // 水平边缘对齐 if (Math.abs(elTop - oTop) < threshold) { guideHPos = oTop; } if (Math.abs(elBottom - oBottom) < threshold) { guideHPos = oBottom; } if (Math.abs(elTop - oBottom) < threshold) { guideHPos = oBottom; } if (Math.abs(elBottom - oTop) < threshold) { guideHPos = oTop; } // 中心对齐 if (Math.abs(elCenterX - oCenterX) < threshold) { guideVPos = oCenterX; } if (Math.abs(elCenterY - oCenterY) < threshold) { guideHPos = oCenterY; } } if (guideHPos !== null) { guideH.style.top = guideHPos + 'px'; guideH.classList.add('visible'); } else { guideH.classList.remove('visible'); } if (guideVPos !== null) { guideV.style.left = guideVPos + 'px'; guideV.classList.add('visible'); } else { guideV.classList.remove('visible'); } } function hideGuides() { guideH.classList.remove('visible'); guideV.classList.remove('visible'); if (gridLines) { gridLines.classList.remove('visible'); } } // 取消所有选中 function deselectAll() { if (editingInput) { endEdit(editingInput); } if (selectedInput) { selectedInput.classList.remove('selected'); selectedInput = null; } // 隐藏编辑器工具栏 editorToolbar.classList.remove('active'); } // 初始化长按保存(只在3:5预览区域) function initLongPressSave() { // 只在图片容器上触发长按保存 imageContainer.addEventListener('touchstart', (e) => { // 如果触摸的是输入框,不触发长按保存 if (e.target.closest('.text-input')) { return; } longPressTimer = setTimeout(() => { if (navigator.vibrate) { navigator.vibrate(50); } saveImage(); }, 800); }, { passive: true }); imageContainer.addEventListener('touchend', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); imageContainer.addEventListener('touchmove', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); } // 初始化右键菜单(只在3:5预览区域) function initContextMenu() { // 只在图片容器上禁用默认右键菜单并显示自定义菜单 imageContainer.addEventListener('contextmenu', (e) => { // 如果右键点击的是输入框,显示默认菜单 if (e.target.closest('.text-input')) { return; } e.preventDefault(); showContextMenu(e.clientX, e.clientY); }); // 点击其他地方关闭菜单 document.addEventListener('click', () => { hideContextMenu(); }); } // 初始化文本编辑器工具栏 function initTextEditorToolbar() { // 加粗按钮 boldBtn.addEventListener('click', () => { const targetInput = editingInput || selectedInput; if (!targetInput) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { document.execCommand('bold', false, null); } else { const isBold = boldBtn.classList.contains('active'); if (isBold) { targetInput.style.fontWeight = 'normal'; } else { targetInput.style.fontWeight = 'bold'; } boldBtn.classList.toggle('active'); } }); // 斜体按钮 italicBtn.addEventListener('click', () => { const targetInput = editingInput || selectedInput; if (!targetInput) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { document.execCommand('italic', false, null); } else { const isItalic = italicBtn.classList.contains('active'); if (isItalic) { targetInput.style.fontStyle = 'normal'; } else { targetInput.style.fontStyle = 'italic'; } italicBtn.classList.toggle('active'); } }); // 颜色选择器 colorPicker.addEventListener('input', () => { const targetInput = editingInput || selectedInput; if (!targetInput) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { document.execCommand('foreColor', false, colorPicker.value); } else { targetInput.style.color = colorPicker.value; } }); // 字体选择器 fontPicker.addEventListener('change', () => { const targetInput = editingInput || selectedInput; if (!targetInput) return; targetInput.style.fontFamily = fontPicker.value; }); // 字号选择器 - 密码锁式滚轮 let fontSizeValue = 16; const fontSizeRoller = document.getElementById('fontSizePicker'); const fontSizeValueEl = fontSizeRoller.querySelector('.roller-value'); const fontSizeUpBtn = fontSizeRoller.querySelector('.roller-up'); const fontSizeDownBtn = fontSizeRoller.querySelector('.roller-down'); function updateFontSize(newSize, forceUpdate = false) { newSize = Math.max(10, Math.min(42, newSize)); const targetInput = editingInput || selectedInput; if (forceUpdate || targetInput) { targetInput.style.fontSize = newSize + 'px'; fontSizeValue = newSize; fontSizeValueEl.textContent = newSize; } else { fontSizeValue = newSize; fontSizeValueEl.textContent = newSize; } } fontSizeUpBtn.addEventListener('click', () => { const targetInput = editingInput || selectedInput; if (targetInput) { const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; updateFontSize(currentSize + 1, true); } }); fontSizeDownBtn.addEventListener('click', () => { const targetInput = editingInput || selectedInput; if (targetInput) { const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; updateFontSize(currentSize - 1, true); } }); fontSizeRoller.addEventListener('wheel', (e) => { e.preventDefault(); const targetInput = editingInput || selectedInput; if (targetInput) { const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; const delta = e.deltaY > 0 ? -1 : 1; updateFontSize(currentSize + delta, true); } }, { passive: false }); } // 初始化键盘微调功能(方向键 1px 移动) function initKeyboardNudge() { document.addEventListener('keydown', (e) => { // 如果正在编辑文本,不处理方向键 if (editingInput) return; // 获取当前选中的元素 const targetEl = selectedInput || selectedOverlay; if (!targetEl) return; // 防止默认行为(页面滚动) if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault(); } let currentLeft = parseInt(targetEl.style.left) || targetEl.offsetLeft; let currentTop = parseInt(targetEl.style.top) || targetEl.offsetTop; switch(e.key) { case 'ArrowUp': currentTop -= 1; break; case 'ArrowDown': currentTop += 1; break; case 'ArrowLeft': currentLeft -= 1; break; case 'ArrowRight': currentLeft += 1; break; default: return; } // 应用新位置(不启用吸附) targetEl.style.left = currentLeft + 'px'; targetEl.style.top = currentTop + 'px'; }); } // 显示右键菜单 function showContextMenu(x, y) { hideContextMenu(); const menu = document.createElement('div'); menu.className = 'context-menu'; menu.id = 'contextMenu'; menu.style.left = x + 'px'; menu.style.top = y + 'px'; const previewItem = document.createElement('div'); previewItem.className = 'context-menu-item'; previewItem.textContent = '预览图片'; previewItem.addEventListener('click', () => { showPreview(); hideContextMenu(); }); const copyItem = document.createElement('div'); copyItem.className = 'context-menu-item'; copyItem.textContent = '复制图片'; copyItem.addEventListener('click', async () => { await copyImageToClipboard(); hideContextMenu(); }); const saveItem = document.createElement('div'); saveItem.className = 'context-menu-item'; saveItem.textContent = '保存图片'; saveItem.addEventListener('click', () => { saveImage(); hideContextMenu(); }); menu.appendChild(previewItem); menu.appendChild(copyItem); menu.appendChild(saveItem); document.body.appendChild(menu); } // 从 blob 显示预览 function showPreviewFromBlob(blob) { console.log('预览图片大小:', (blob.size / 1024).toFixed(2), 'KB'); const dataUrl = URL.createObjectURL(blob); showPreviewModal(dataUrl, blob); } // 显示预览 async function showPreview() { const canvas = await generateCanvas(); console.log('预览画布尺寸:', canvas.width, 'x', canvas.height); // 尝试使用 toDataURL 获取更高质量的图片 let dataUrl; let blob; try { // 先尝试 toDataURL dataUrl = canvas.toDataURL('image/png'); console.log('预览 toDataURL 成功,dataUrl 长度:', dataUrl.length); // 将 dataUrl 转换为 blob const response = await fetch(dataUrl); blob = await response.blob(); console.log('预览 dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes'); } catch (err) { console.log('预览 toDataURL 失败,使用 toBlob:', err); // 如果 toDataURL 失败,使用 toBlob blob = await new Promise(resolve => { canvas.toBlob((b) => { console.log('预览 toBlob 回调 - blob 尺寸:', b.size, 'bytes'); resolve(b); }, 'image/png', 1.0); }); dataUrl = URL.createObjectURL(blob); } console.log('预览图片大小:', (blob.size / 1024).toFixed(2), 'KB'); showPreviewModal(dataUrl, blob); } // 显示预览弹窗 function showPreviewModal(dataUrl, blob) { const tipOverlay = document.createElement('div'); tipOverlay.id = 'previewTip'; tipOverlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10000; padding: 20px; `; const tipText = document.createElement('p'); tipText.innerHTML = '长按图片保存到相册
(预览已缩放,实际保存为高清原图)'; tipText.style.cssText = ` color: #fff; font-size: 18px; margin-bottom: 20px; text-align: center; `; const img = document.createElement('img'); img.src = dataUrl; img.style.cssText = ` max-width: 100%; max-height: 70vh; border-radius: 8px; box-shadow: 0 4px 20px rgba(255,255,255,0.2); -webkit-touch-callout: default; -webkit-user-select: auto; user-select: auto; `; const btnContainer = document.createElement('div'); btnContainer.style.cssText = ` display: flex; gap: 12px; margin-top: 20px; `; const saveBtn = document.createElement('button'); saveBtn.textContent = '保存到相册'; saveBtn.style.cssText = ` padding: 12px 30px; font-size: 16px; background: #007AFF; color: #fff; border: none; border-radius: 25px; cursor: pointer; `; saveBtn.onclick = async () => { tipOverlay.remove(); await saveImageToGallery(dataUrl, blob); // 重新隐藏 canvas exportCanvas.setAttribute('hidden', ''); exportCanvas.style.position = ''; exportCanvas.style.left = ''; exportCanvas.style.top = ''; }; const closeBtn = document.createElement('button'); closeBtn.textContent = '关闭'; closeBtn.style.cssText = ` padding: 12px 30px; font-size: 16px; background: #fff; color: #333; border: none; border-radius: 25px; cursor: pointer; `; closeBtn.onclick = () => { tipOverlay.remove(); // 重新隐藏 canvas exportCanvas.setAttribute('hidden', ''); exportCanvas.style.position = ''; exportCanvas.style.left = ''; exportCanvas.style.top = ''; }; btnContainer.appendChild(saveBtn); btnContainer.appendChild(closeBtn); tipOverlay.appendChild(tipText); tipOverlay.appendChild(img); tipOverlay.appendChild(btnContainer); document.body.appendChild(tipOverlay); } // 保存图片到相册 async function saveImageToGallery(dataUrl, blob) { const filename = '邀请函_' + Date.now() + '.png'; // 如果没有传入 blob,尝试从 window._lastExportBlob 获取 if (!blob && window._lastExportBlob) { blob = window._lastExportBlob; } // 尝试使用 File System Access API (Android/Chrome) if ('showSaveFilePicker' in window) { try { const handle = await window.showSaveFilePicker({ suggestedName: filename, types: [{ description: 'PNG 图片', accept: { 'image/png': ['.png'] } }] }); const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); showToast('图片已保存到相册'); return; } catch (err) { console.log('File System Access API 失败,使用传统方式'); } } // 尝试使用 Web Share API (iOS Safari) if (navigator.share && navigator.canShare) { try { const file = new File([blob], filename, { type: 'image/png' }); if (navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: '邀请函', text: '保存邀请函图片' }); showToast('图片已分享'); return; } } catch (err) { console.log('Web Share API 失败,使用传统方式'); } } // 传统下载方式 const link = document.createElement('a'); link.download = filename; link.href = dataUrl; document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast('图片已保存,请在相册中查看'); } // 隐藏右键菜单 function hideContextMenu() { const existingMenu = document.getElementById('contextMenu'); if (existingMenu) { existingMenu.remove(); } } // 复制图片到剪贴板 async function copyImageToClipboard() { if (!currentImage) { alert('请先添加背景图片'); return; } try { deselectAll(); const canvas = await generateCanvas(); const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]); alert('图片已复制到剪贴板'); } catch (err) { console.error('复制失败:', err); alert('复制失败,请尝试保存图片'); } } // 检测是否在微信内置浏览器 function isWechat() { return /MicroMessenger/i.test(navigator.userAgent); } // 生成画布(高清无损,保持与显示容器相同的宽高比) async function generateCanvas() { const canvas = exportCanvas; const container = imageContainer; // 获取显示容器的实际尺寸和宽高比 const displayWidth = container.clientWidth; const displayHeight = container.clientHeight; const displayRatio = displayWidth / displayHeight; // 根据设备像素比决定输出分辨率,移动端至少 2x const dpr = window.devicePixelRatio || 1; const minScale = Math.max(dpr, 2); // 至少 2x // 先计算目标尺寸 let TARGET_WIDTH = Math.round(displayWidth * minScale); let TARGET_HEIGHT = Math.round(displayHeight * minScale); // 微信特殊处理:微信内置浏览器 canvas 限制更严格 // 实测微信中 canvas 最大尺寸约为 4096,但超过 2048 可能有问题 const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096; // 如果超出限制,按宽高比等比例缩放 if (TARGET_HEIGHT > MAX_CANVAS_SIZE || TARGET_WIDTH > MAX_CANVAS_SIZE) { const maxDimension = Math.max(TARGET_WIDTH, TARGET_HEIGHT); const scaleFactor = MAX_CANVAS_SIZE / maxDimension; TARGET_WIDTH = Math.round(TARGET_WIDTH * scaleFactor); TARGET_HEIGHT = Math.round(TARGET_HEIGHT * scaleFactor); } // 确保最小分辨率,同时保持宽高比 const minWidth = 1080; const minHeight = 1920; if (TARGET_WIDTH < minWidth || TARGET_HEIGHT < minHeight) { const widthScale = minWidth / TARGET_WIDTH; const heightScale = minHeight / TARGET_HEIGHT; const scaleFactor = Math.max(widthScale, heightScale); TARGET_WIDTH = Math.round(TARGET_WIDTH * scaleFactor); TARGET_HEIGHT = Math.round(TARGET_HEIGHT * scaleFactor); // 再次检查是否超出限制 if (TARGET_HEIGHT > MAX_CANVAS_SIZE || TARGET_WIDTH > MAX_CANVAS_SIZE) { const maxDimension = Math.max(TARGET_WIDTH, TARGET_HEIGHT); const limitScale = MAX_CANVAS_SIZE / maxDimension; TARGET_WIDTH = Math.round(TARGET_WIDTH * limitScale); TARGET_HEIGHT = Math.round(TARGET_HEIGHT * limitScale); } } console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat()); console.log('显示容器尺寸:', displayWidth, 'x', displayHeight); // 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制 canvas.removeAttribute('hidden'); canvas.style.position = 'fixed'; canvas.style.left = '-9999px'; canvas.style.top = '-9999px'; // 设置画布为高清分辨率 canvas.width = TARGET_WIDTH; canvas.height = TARGET_HEIGHT; // 微信中:验证 canvas 尺寸是否设置成功 if (isWechat() && (canvas.width !== TARGET_WIDTH || canvas.height !== TARGET_HEIGHT)) { console.warn('Canvas 尺寸设置失败,使用实际尺寸:', canvas.width, 'x', canvas.height); TARGET_WIDTH = canvas.width; TARGET_HEIGHT = canvas.height; } console.log('实际画布尺寸:', canvas.width, 'x', canvas.height); // 获取 2d 上下文(必须在设置尺寸之后) const ctx = canvas.getContext('2d'); // 启用最高质量渲染 ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // 白色背景 ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT); // 计算从显示尺寸到目标分辨率的缩放比例 // 使用统一的缩放比例,保持宽高比,确保所见即所得 // 注意:必须在 canvas 尺寸验证之后计算 scale const scale = TARGET_WIDTH / displayWidth; console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio); if (currentImage) { console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight); drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scale); } await drawOverlayImagesHD(ctx, scale); // 等待所有字体加载完成 const fontPromises = []; const textInputs = container.querySelectorAll('.preview-input-layer .text-input'); textInputs.forEach(textInput => { const style = window.getComputedStyle(textInput); const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '').trim(); if (fontFamily && document.fonts) { fontPromises.push(document.fonts.load('16px ' + fontFamily).catch(() => {})); } }); // 最多等待 500ms await Promise.race([ Promise.all(fontPromises), new Promise(resolve => setTimeout(resolve, 500)) ]); textInputs.forEach(textInput => { drawTextOnCanvasHD(ctx, textInput, scale); }); return canvas; } // 绘制背景图 function drawBgImageOnCanvas(ctx, img, width, height) { // 使用与编辑时完全相同的计算逻辑 const containerWidth = width; const containerHeight = height; const imgRatio = img.naturalWidth / img.naturalHeight; const containerRatio = containerWidth / containerHeight; let imgWidth, imgHeight; if (imgRatio > containerRatio) { imgHeight = containerHeight; imgWidth = imgHeight * imgRatio; } else { imgWidth = containerWidth; imgHeight = imgWidth / imgRatio; } // 应用缩放 const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; // 计算位置(与 updateBgImage 函数完全一致) const baseLeft = (containerWidth - scaledWidth) / 2; const drawX = baseLeft + imageOffsetX; const drawY = (containerHeight - scaledHeight) / 2; ctx.globalAlpha = bgImageOpacity; ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); ctx.globalAlpha = 1; } // 绘制背景图(高清版本) function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scale) { // 先按显示容器的尺寸计算(与 updateBgImage 完全一致) const displayContainer = imageContainer; const displayWidth = displayContainer.clientWidth; const displayHeight = displayContainer.clientHeight; const imgRatio = img.naturalWidth / img.naturalHeight; const displayRatio = displayWidth / displayHeight; let imgWidth, imgHeight; if (imgRatio > displayRatio) { imgHeight = displayHeight; imgWidth = imgHeight * imgRatio; } else { imgWidth = displayWidth; imgHeight = imgWidth / imgRatio; } // 应用缩放(与编辑时的 bgImageScale 一致) const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; // 计算位置(与 updateBgImage 函数完全一致) const baseLeft = (displayWidth - scaledWidth) / 2; const displayDrawX = baseLeft + imageOffsetX; const displayDrawY = (displayHeight - scaledHeight) / 2; // 将显示坐标转换为 canvas 坐标 const drawX = displayDrawX * scale; const drawY = displayDrawY * scale; const canvasScaledWidth = scaledWidth * scale; const canvasScaledHeight = scaledHeight * scale; ctx.globalAlpha = bgImageOpacity; // 调试信息 console.log('背景图绘制:', { canvas: canvasWidth + 'x' + canvasHeight, scale: scale, displayDrawX: displayDrawX, displayDrawY: displayDrawY, drawX: drawX, drawY: drawY, canvasScaledWidth: canvasScaledWidth, canvasScaledHeight: canvasScaledHeight }); ctx.drawImage(img, drawX, drawY, canvasScaledWidth, canvasScaledHeight); ctx.globalAlpha = 1; } // 绘制层叠图 function drawOverlayImages(ctx) { return new Promise((resolve) => { const overlayImages = overlayLayer.querySelectorAll('.overlay-image'); if (overlayImages.length === 0) { resolve(); return; } let loadedCount = 0; overlayImages.forEach(overlayEl => { const imgEl = overlayEl.querySelector('img'); if (!imgEl) { loadedCount++; return; } // 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置 const left = overlayEl.offsetLeft; const top = overlayEl.offsetTop; const width = overlayEl.offsetWidth; const height = overlayEl.offsetHeight; ctx.globalAlpha = overlayEl.style.opacity || 1; const img = new Image(); img.onload = () => { ctx.drawImage(img, left, top, width, height); ctx.globalAlpha = 1; loadedCount++; if (loadedCount >= overlayImages.length) { resolve(); } }; img.onerror = () => { ctx.globalAlpha = 1; loadedCount++; if (loadedCount >= overlayImages.length) { resolve(); } }; img.src = imgEl.src; }); }); } // 绘制层叠图(高清版本) function drawOverlayImagesHD(ctx, scale) { return new Promise((resolve) => { const overlayImages = overlayLayer.querySelectorAll('.overlay-image'); if (overlayImages.length === 0) { resolve(); return; } let loadedCount = 0; const totalImages = overlayImages.length; function checkComplete() { loadedCount++; if (loadedCount >= totalImages) { resolve(); } } overlayImages.forEach(overlayEl => { const imgEl = overlayEl.querySelector('img'); if (!imgEl) { checkComplete(); return; } // 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置,并应用统一缩放 const left = overlayEl.offsetLeft * scale; const top = overlayEl.offsetTop * scale; const width = overlayEl.offsetWidth * scale; const height = overlayEl.offsetHeight * scale; ctx.globalAlpha = overlayEl.style.opacity || 1; // 直接使用已加载的图片元素,避免重新加载导致质量下降 if (imgEl.complete && imgEl.naturalWidth > 0) { ctx.drawImage(imgEl, left, top, width, height); ctx.globalAlpha = 1; checkComplete(); } else { // 如果图片未加载完成,等待加载 imgEl.onload = () => { ctx.drawImage(imgEl, left, top, width, height); ctx.globalAlpha = 1; checkComplete(); }; imgEl.onerror = () => { ctx.globalAlpha = 1; checkComplete(); }; } }); }); } // 在画布上绘制文字 function drawTextOnCanvas(ctx, inputElement) { const deleteBtn = inputElement.querySelector('.text-delete-btn'); let text = inputElement.textContent; if (deleteBtn) { text = text.replace(deleteBtn.textContent, ''); } if (!text.trim()) return; // 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置 const inputLeft = inputElement.offsetLeft; const inputTop = inputElement.offsetTop; const style = window.getComputedStyle(inputElement); const fontSize = parseInt(style.fontSize); const lineHeight = parseFloat(style.lineHeight); ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`; ctx.fillStyle = style.color; ctx.textBaseline = 'alphabetic'; // 计算文本垂直偏移量,使其与 DOM 渲染一致 // DOM 中 line-height 会影响文本位置,需要计算实际文本基线位置 const actualLineHeight = lineHeight || fontSize * 1.4; const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; ctx.fillText(text, inputLeft, inputTop + textOffsetY); } // 在画布上绘制文字(高清版本) function drawTextOnCanvasHD(ctx, inputElement, scale) { const deleteBtn = inputElement.querySelector('.text-delete-btn'); let text = inputElement.textContent; if (deleteBtn) { text = text.replace(deleteBtn.textContent, ''); } if (!text.trim()) return; // 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置,并应用统一缩放 const inputLeft = inputElement.offsetLeft * scale; const inputTop = inputElement.offsetTop * scale; // 强制刷新样式,确保获取最新计算值 inputElement.getBoundingClientRect(); const style = window.getComputedStyle(inputElement); const originalFontSize = parseInt(style.fontSize); const fontSize = originalFontSize * scale; // 字体大小也按比例缩放 // lineHeight 可能是像素值或数字(倍数),需要正确处理 let lineHeight = parseFloat(style.lineHeight); // 如果 lineHeight 是数字(小于 10 认为是倍数),转换为像素值 if (lineHeight && lineHeight < 10) { lineHeight = lineHeight * originalFontSize * scale; } else if (lineHeight) { // 已经是像素值,按比例缩放 lineHeight = lineHeight * scale; } // 处理字体粗细 - 统一转换为标准格式 let fontWeight = style.fontWeight; // 数值转换为关键字 if (fontWeight === '400' || fontWeight === 'normal') fontWeight = 'normal'; else if (fontWeight === '700' || fontWeight === 'bold') fontWeight = 'bold'; // 获取斜体样式 let fontStyle = style.fontStyle; if (fontStyle === 'oblique') fontStyle = 'italic'; else if (fontStyle !== 'italic') fontStyle = 'normal'; // 清理字体名称中的引号 let fontFamily = style.fontFamily.replace(/['"]/g, ''); // 确保字体堆栈有效 if (!fontFamily.includes('sans-serif') && !fontFamily.includes('serif') && !fontFamily.includes('monospace')) { fontFamily += ', sans-serif'; } // 使用高清字体渲染 - 按标准顺序:style weight size family const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; ctx.font = fontString; ctx.fillStyle = style.color; // 调试:输出实际应用的字体样式 console.log('文本绘制样式:', { text: text.substring(0, 10) + (text.length > 10 ? '...' : ''), fontString: fontString, fontSize: fontSize, fontWeight: fontWeight, fontStyle: fontStyle, fontFamily: fontFamily, originalFontFamily: style.fontFamily, color: style.color, actualFont: ctx.font, isWechat: isWechat() }); ctx.textBaseline = 'alphabetic'; // 微信中:开启文字抗锯齿 if (isWechat()) { ctx.textRendering = 'optimizeLegibility'; } // 计算文本垂直偏移量,使其与 DOM 渲染一致 // 使用缩放后的 lineHeight 计算 const actualLineHeight = lineHeight || fontSize * 1.4; const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; ctx.fillText(text, inputLeft, inputTop + textOffsetY); } // 保存图片 async function saveImage() { if (!currentImage) { alert('请先添加背景图片'); return; } try { const canvas = await generateCanvas(); await showMobileSaveTip(canvas); } catch (err) { console.error('保存失败:', err); alert('保存失败,请重试'); } } // 显示移动端保存选项 async function showMobileSaveTip(canvas) { console.log('导出画布尺寸:', canvas.width, 'x', canvas.height); // 在 toBlob 之前再次确认 canvas 尺寸 console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height); // 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损 let dataUrl; let blob; // 微信中优先使用 toBlob,避免 toDataURL 的内存问题 if (isWechat()) { try { blob = await new Promise((resolve, reject) => { canvas.toBlob((b) => { if (b) { console.log('微信 toBlob 成功,blob 尺寸:', b.size, 'bytes'); resolve(b); } else { reject(new Error('toBlob 返回 null')); } }, 'image/png', 1.0); // 1.0 = 最高质量无损 PNG }); dataUrl = URL.createObjectURL(blob); } catch (err) { console.log('微信 toBlob 失败,回退到 toDataURL:', err); dataUrl = canvas.toDataURL('image/png'); const response = await fetch(dataUrl); blob = await response.blob(); } } else { // 非微信环境,先尝试 toDataURL try { dataUrl = canvas.toDataURL('image/png'); console.log('toDataURL 成功,dataUrl 长度:', dataUrl.length); const response = await fetch(dataUrl); blob = await response.blob(); console.log('dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes'); } catch (err) { console.log('toDataURL 失败,使用 toBlob:', err); blob = await new Promise(resolve => { canvas.toBlob((b) => { console.log('toBlob 回调 - blob 尺寸:', b.size, 'bytes'); resolve(b); }, 'image/png', 1.0); }); dataUrl = URL.createObjectURL(blob); } } console.log('导出图片大小:', (blob.size / 1024).toFixed(2), 'KB'); const filename = '邀请函_' + Date.now() + '.png'; // 保存 blob 供后续使用 window._lastExportBlob = blob; const tipOverlay = document.createElement('div'); tipOverlay.id = 'mobileSaveTip'; tipOverlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: flex-end; justify-content: center; z-index: 10000; `; const savePanel = document.createElement('div'); savePanel.style.cssText = ` width: 100%; background: #fff; border-radius: 16px 16px 0 0; padding: 20px; padding-bottom: 40px; `; const title = document.createElement('p'); title.textContent = '保存图片'; title.style.cssText = ` font-size: 18px; font-weight: bold; text-align: center; margin-bottom: 20px; color: #333; `; const saveBtn = document.createElement('button'); saveBtn.textContent = '保存到相册'; saveBtn.style.cssText = ` width: 100%; padding: 16px; font-size: 16px; background: #007AFF; color: #fff; border: none; border-radius: 10px; cursor: pointer; margin-bottom: 12px; `; saveBtn.onclick = async () => { tipOverlay.remove(); await saveImageToGallery(dataUrl, blob); }; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = ` width: 100%; padding: 16px; font-size: 16px; background: #F2F2F7; color: #333; border: none; border-radius: 10px; cursor: pointer; `; cancelBtn.onclick = () => { tipOverlay.remove(); // 重新隐藏 canvas exportCanvas.setAttribute('hidden', ''); exportCanvas.style.position = ''; exportCanvas.style.left = ''; exportCanvas.style.top = ''; }; tipOverlay.addEventListener('click', (e) => { if (e.target === tipOverlay) { tipOverlay.remove(); // 重新隐藏 canvas exportCanvas.setAttribute('hidden', ''); exportCanvas.style.position = ''; exportCanvas.style.left = ''; exportCanvas.style.top = ''; } }); savePanel.appendChild(title); savePanel.appendChild(saveBtn); savePanel.appendChild(cancelBtn); tipOverlay.appendChild(savePanel); document.body.appendChild(tipOverlay); // 添加预览按钮 const previewBtn = document.createElement('button'); previewBtn.textContent = '预览图片'; previewBtn.style.cssText = ` width: 100%; padding: 16px; font-size: 16px; background: #34C759; color: #fff; border: none; border-radius: 10px; cursor: pointer; margin-bottom: 12px; `; previewBtn.onclick = () => { tipOverlay.remove(); showPreviewFromBlob(blob); }; // 插入到标题后面 savePanel.insertBefore(previewBtn, saveBtn); } // 显示 Toast 提示 function showToast(message) { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: #fff; padding: 16px 24px; border-radius: 8px; font-size: 14px; z-index: 10001; pointer-events: none; animation: fadeInOut 2s ease forwards; `; toast.textContent = message; // 添加动画样式 if (!document.getElementById('toastStyle')) { const style = document.createElement('style'); style.id = 'toastStyle'; style.textContent = ` @keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } } `; document.head.appendChild(style); } document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } // 窗口大小改变时更新图片位置和边界 window.addEventListener('resize', () => { if (currentImage) { calculateImageBounds(); updateImagePosition(); } }); })();