// 极简邀请函制作工具 (function() { const fileInput = document.getElementById('fileInput'); const addImageBtn = document.getElementById('addImageBtn'); 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 exportCanvas = document.getElementById('exportCanvas'); const longPressTip = document.getElementById('longPressTip'); const canvasArea = document.getElementById('canvasArea'); const textEditorToolbar = document.getElementById('textEditorToolbar'); 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'); let currentImage = null; 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; // 初始化 init(); function init() { // 添加图片按钮 addImageBtn.addEventListener('click', () => { fileInput.click(); }); // 文件选择 fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { loadImage(file); } fileInput.value = ''; }); // 保存按钮 saveBtn.addEventListener('click', saveImage); // 初始化输入框 initInput(input1); initInput(input2); // 初始化图片滑动 initImageDrag(); // 点击空白区域取消选中 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 isTextEditorToolbar = e.target.closest('.text-editor-toolbar'); if (!isTextInput && !isImageContainer && !isToolbar && !isTextEditorToolbar) { deselectAll(); } }); // 点击容器空白处取消选中 imageContainer.addEventListener('click', (e) => { // 点击容器、背景图层或背景图片,取消输入框选中 if (e.target === imageContainer || e.target === bgLayer || e.target === bgImage || e.target.closest('.preview-bg-layer')) { deselectAll(); } }); // 初始化长按保存(只在3:5预览区域) initLongPressSave(); // 初始化右键菜单(只在3:5预览区域) initContextMenu(); // 初始化文本编辑器工具栏 initTextEditorToolbar(); // 显示长按提示 setTimeout(() => { longPressTip.classList.add('show'); setTimeout(() => { longPressTip.classList.remove('show'); }, 3000); }, 1000); } // 加载图片 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; updateImagePosition(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } // 计算图片滑动边界 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 updateImagePosition() { if (!currentImage) return; // 限制滑动范围,确保不露出白板 imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX)); 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; } // 应用位置 const baseLeft = (containerWidth - imgWidth) / 2; bgImage.style.width = imgWidth + 'px'; bgImage.style.height = imgHeight + 'px'; bgImage.style.left = (baseLeft + imageOffsetX) + 'px'; 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); 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; imageStartX = clientX; imageStartOffsetX = imageOffsetX; 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; } } // 初始化输入框 - 每个输入框独立管理自己的拖拽状态 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; // 鼠标/触摸按下 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; startLeft = element.offsetLeft; startTop = element.offsetTop; // 延迟判断是否为拖拽 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) ); if (moveDistance > 5) { 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; element.style.left = (startLeft + dx) + 'px'; element.style.top = (startTop + dy) + 'px'; } // 阻止事件冒泡,避免触发图片滑动 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'); } // 阻止事件冒泡 if (e) { e.stopPropagation(); } } // 双击进入编辑模式(电脑端和移动端通用) element.addEventListener('dblclick', (e) => { e.stopPropagation(); startEdit(element); }); // 触摸双击检测(移动端双击进入编辑) let lastTapTime = 0; element.addEventListener('touchend', (e) => { const currentTime = Date.now(); if (currentTime - lastTapTime < 300) { e.stopPropagation(); startEdit(element); } lastTapTime = currentTime; }); } // 选中输入框 function selectInput(element) { 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'; } // 开始编辑 function startEdit(element) { if (editingInput && editingInput !== element) { endEdit(editingInput); } editingInput = element; selectedInput = element; element.classList.add('editing'); element.classList.remove('selected'); element.contentEditable = 'true'; // 更新工具栏状态 updateToolbarState(element); // 如果没有选中文本,则定位光标到末尾 setTimeout(() => { const sel = window.getSelection(); if (sel.isCollapsed) { const range = document.createRange(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } element.focus(); }, 10); if ('virtualKeyboard' in navigator) { navigator.virtualKeyboard.show(); } } // 结束编辑 function endEdit(element) { element.classList.remove('editing'); element.contentEditable = 'false'; element.blur(); editingInput = null; } // 更新工具栏状态 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); // 更新字体选择器 for (let option of fontPicker.options) { if (option.value === fontFamily || fontFamily.includes(option.value)) { fontPicker.value = option.value; break; } } // 更新字号选择器 fontSizePicker.value = fontSize.toString(); } // 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 deselectAll() { if (editingInput) { endEdit(editingInput); } if (selectedInput) { selectedInput.classList.remove('selected'); selectedInput = null; } } // 初始化长按保存(只在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', () => { if (!editingInput) 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) { editingInput.style.fontWeight = 'normal'; } else { editingInput.style.fontWeight = 'bold'; } boldBtn.classList.toggle('active'); } }); // 斜体按钮 italicBtn.addEventListener('click', () => { if (!editingInput) 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) { editingInput.style.fontStyle = 'normal'; } else { editingInput.style.fontStyle = 'italic'; } italicBtn.classList.toggle('active'); } }); // 颜色选择器 colorPicker.addEventListener('input', () => { if (!editingInput) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { document.execCommand('foreColor', false, colorPicker.value); } else { editingInput.style.color = colorPicker.value; } }); // 字体选择器 fontPicker.addEventListener('change', () => { if (!editingInput) return; editingInput.style.fontFamily = fontPicker.value + ', sans-serif'; }); // 字号选择器 fontSizePicker.addEventListener('change', () => { if (!editingInput) return; editingInput.style.fontSize = fontSizePicker.value + '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); } // 显示预览 async function showPreview() { const canvas = await generateCanvas(); const dataUrl = canvas.toDataURL('image/png'); 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.textContent = '预览图片'; 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: 80vh; border-radius: 8px; box-shadow: 0 4px 20px rgba(255,255,255,0.2); `; const closeBtn = document.createElement('button'); closeBtn.textContent = '关闭'; closeBtn.style.cssText = ` margin-top: 20px; padding: 12px 40px; font-size: 16px; background: #fff; color: #333; border: none; border-radius: 25px; cursor: pointer; `; closeBtn.onclick = () => tipOverlay.remove(); tipOverlay.appendChild(tipText); tipOverlay.appendChild(img); tipOverlay.appendChild(closeBtn); document.body.appendChild(tipOverlay); } // 隐藏右键菜单 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('复制失败,请尝试保存图片'); } } // 生成画布(预览与保存样式一致) async function generateCanvas() { const canvas = exportCanvas; const ctx = canvas.getContext('2d'); const containerWidth = imageContainer.clientWidth; const containerHeight = imageContainer.clientHeight; canvas.width = containerWidth; canvas.height = containerHeight; // 绘制白色背景 ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制背景图(考虑滑动偏移) if (currentImage) { drawImageWithOffset(ctx, currentImage, containerWidth, containerHeight); } // 绘制文字 drawTextOnCanvas(ctx, input1); drawTextOnCanvas(ctx, input2); return canvas; } // 绘制图片(考虑滑动偏移) function drawImageWithOffset(ctx, img, containerWidth, containerHeight) { 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 baseLeft = (containerWidth - imgWidth) / 2; const drawX = baseLeft + imageOffsetX; ctx.drawImage(img, drawX, 0, imgWidth, imgHeight); } // 在画布上绘制文字 function drawTextOnCanvas(ctx, inputElement) { const text = inputElement.textContent; if (!text) return; const style = window.getComputedStyle(inputElement); const inputLeft = inputElement.offsetLeft; const inputTop = inputElement.offsetTop; const fontSize = parseInt(style.fontSize); ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`; ctx.fillStyle = style.color; ctx.textBaseline = 'top'; ctx.fillText(text, inputLeft, inputTop); } // 保存图片 async function saveImage() { if (!currentImage) { alert('请先添加背景图片'); return; } try { const canvas = await generateCanvas(); showMobileSaveTip(); } catch (err) { console.error('保存失败:', err); alert('保存失败,请重试'); } } // 显示移动端保存选项 function showMobileSaveTip() { const canvas = exportCanvas; const dataUrl = canvas.toDataURL('image/png'); const filename = '邀请函_' + Date.now() + '.png'; 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 = () => { tipOverlay.remove(); const link = document.createElement('a'); link.download = filename; link.href = dataUrl; link.click(); }; 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(); tipOverlay.addEventListener('click', (e) => { if (e.target === tipOverlay) { tipOverlay.remove(); } }); 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(); showPreview(); }; // 插入到标题后面 savePanel.insertBefore(previewBtn, saveBtn); } // 窗口大小改变时更新图片位置和边界 window.addEventListener('resize', () => { if (currentImage) { calculateImageBounds(); updateImagePosition(); } }); })();