diff --git a/css/style.css b/css/style.css index e49d0a1..49a8b74 100644 --- a/css/style.css +++ b/css/style.css @@ -513,23 +513,3 @@ html, body { .guide-line.visible { display: block; } - -/* 1px 网格线 */ -.grid-lines { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - z-index: 5; - display: none; - background-image: - linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px), - linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px); - background-size: 10px 10px; -} - -.grid-lines.visible { - display: block; -} diff --git a/index.html b/index.html index 81a0974..8e7937a 100644 --- a/index.html +++ b/index.html @@ -80,8 +80,6 @@
- -
diff --git a/js/script.js b/js/script.js index 2c7e68d..78eaff3 100644 --- a/js/script.js +++ b/js/script.js @@ -38,7 +38,6 @@ 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; @@ -208,9 +207,6 @@ // 初始化文本编辑器工具栏 initTextEditorToolbar(); - // 初始化键盘微调功能 - initKeyboardNudge(); - // 加载默认背景图 loadDefaultBackground(); @@ -454,23 +450,27 @@ const dx = clientX - startX; const dy = clientY - startY; - // 移除移动距离限制,实现 1px 精确移动 - let newLeft = startLeft + dx; - let newTop = startTop + dy; + const moveDistance = Math.sqrt(dx * dx + dy * dy); + if (moveDistance > 5) { + let newLeft = startLeft + dx; + let newTop = startTop + dy; - // 使用吸附功能 - const snapped = getSnapPosition(overlayEl, newLeft, newTop); - newLeft = snapped.left; - newTop = snapped.top; + // 使用吸附功能 + const snapped = getSnapPosition(overlayEl, newLeft, newTop); + newLeft = snapped.left; + newTop = snapped.top; - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) - if (snapped.showGuide) { - showAlignmentGuides(overlayEl); + // 显示对齐辅助线 + if (snapped.showGuide) { + showAlignmentGuides(overlayEl); + } else { + hideGuides(); + } + + overlayEl.style.left = newLeft + 'px'; + overlayEl.style.top = newTop + 'px'; } - overlayEl.style.left = newLeft + 'px'; - overlayEl.style.top = newTop + 'px'; - e.preventDefault(); } @@ -974,8 +974,7 @@ Math.pow(clientY - clickStartY, 2) ); - // 只要有移动就开始拖拽,实现 1px 精确移动 - if (moveDistance > 0) { + if (moveDistance > 5) { hasMoved = true; if (dragTimer) { clearTimeout(dragTimer); @@ -998,9 +997,11 @@ element.style.left = snapped.left + 'px'; element.style.top = snapped.top + 'px'; - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) + // 显示对齐辅助线 if (snapped.showGuide) { showAlignmentGuides(element); + } else { + hideGuides(); } } @@ -1216,16 +1217,11 @@ const cCenterX = cWidth / 2; const cCenterY = cHeight / 2; - const threshold = 1; + const threshold = 5; let snapLeft = newLeft; let snapTop = newTop; let showGuide = false; - // 显示网格线 - if (gridLines) { - gridLines.classList.add('visible'); - } - // 水平垂直分别检测 let snapX = false; let snapY = false; @@ -1261,7 +1257,7 @@ } // 检测其他元素对齐(输入框和层叠图) - const otherInputs = [input1, input2, input3].filter(el => el && el !== draggedEl); + const otherInputs = [input1, input2].filter(el => el && el !== draggedEl); const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image')) .filter(el => el !== draggedEl); @@ -1424,7 +1420,7 @@ const elCenterX = elLeft + elWidth / 2; const elCenterY = elTop + elHeight / 2; - const threshold = 1; + const threshold = 5; let guideHPos = null; let guideVPos = null; @@ -1575,9 +1571,6 @@ function hideGuides() { guideH.classList.remove('visible'); guideV.classList.remove('visible'); - if (gridLines) { - gridLines.classList.remove('visible'); - } } // 取消所有选中 @@ -1745,53 +1738,6 @@ }, { 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'; - - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) - const snapped = getSnapPosition(targetEl, currentLeft, currentTop); - if (snapped.showGuide) { - showAlignmentGuides(targetEl); - } - }); - } - // 显示右键菜单 function showContextMenu(x, y) { hideContextMenu(); @@ -1832,49 +1778,10 @@ 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 dataUrl = canvas.toDataURL('image/png'); const tipOverlay = document.createElement('div'); tipOverlay.id = 'previewTip'; @@ -1894,7 +1801,7 @@ `; const tipText = document.createElement('p'); - tipText.innerHTML = '长按图片保存到相册
(预览已缩放,实际保存为高清原图)'; + tipText.textContent = '长按图片保存到相册'; tipText.style.cssText = ` color: #fff; font-size: 18px; @@ -1934,12 +1841,7 @@ `; saveBtn.onclick = async () => { tipOverlay.remove(); - await saveImageToGallery(dataUrl, blob); - // 重新隐藏 canvas - exportCanvas.setAttribute('hidden', ''); - exportCanvas.style.position = ''; - exportCanvas.style.left = ''; - exportCanvas.style.top = ''; + await saveImageToGallery(dataUrl); }; const closeBtn = document.createElement('button'); @@ -1953,14 +1855,7 @@ border-radius: 25px; cursor: pointer; `; - closeBtn.onclick = () => { - tipOverlay.remove(); - // 重新隐藏 canvas - exportCanvas.setAttribute('hidden', ''); - exportCanvas.style.position = ''; - exportCanvas.style.left = ''; - exportCanvas.style.top = ''; - }; + closeBtn.onclick = () => tipOverlay.remove(); btnContainer.appendChild(saveBtn); btnContainer.appendChild(closeBtn); @@ -1971,17 +1866,13 @@ } // 保存图片到相册 - async function saveImageToGallery(dataUrl, blob) { + async function saveImageToGallery(dataUrl) { 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 blob = await (await fetch(dataUrl)).blob(); const handle = await window.showSaveFilePicker({ suggestedName: filename, types: [{ @@ -2002,6 +1893,7 @@ // 尝试使用 Web Share API (iOS Safari) if (navigator.share && navigator.canShare) { try { + const blob = await (await fetch(dataUrl)).blob(); const file = new File([blob], filename, { type: 'image/png' }); if (navigator.canShare({ files: [file] })) { @@ -2060,57 +1952,30 @@ } } - // 生成画布(高清无损,固定 1080×1920 分辨率) + // 生成画布(所见即所得 - 使用显示尺寸) async function generateCanvas() { const canvas = exportCanvas; const ctx = canvas.getContext('2d'); const container = imageContainer; - // 固定高清分辨率 1080×1920 - const TARGET_WIDTH = 1080; - const TARGET_HEIGHT = 1920; - - console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT); - - // 显示尺寸(用于计算缩放比例) - const displayWidth = container.clientWidth; - const displayHeight = container.clientHeight; - 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; - console.log('实际画布尺寸:', canvas.width, 'x', canvas.height); + const width = container.clientWidth; + const height = container.clientHeight; - // 启用最高质量渲染 - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; + canvas.width = width; + canvas.height = height; - // 白色背景 ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT); - - // 计算从显示尺寸到目标分辨率的缩放比例 - const scaleX = TARGET_WIDTH / displayWidth; - const scaleY = TARGET_HEIGHT / displayHeight; - console.log('缩放比例:', scaleX, 'x', scaleY); + ctx.fillRect(0, 0, width, height); if (currentImage) { - console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight); - drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scaleX, scaleY); + drawBgImageOnCanvas(ctx, currentImage, width, height); } - await drawOverlayImagesHD(ctx, scaleX, scaleY); + await drawOverlayImages(ctx); const textInputs = container.querySelectorAll('.preview-input-layer .text-input'); textInputs.forEach(textInput => { - drawTextOnCanvasHD(ctx, textInput, scaleX, scaleY); + drawTextOnCanvas(ctx, textInput); }); return canvas; @@ -2148,39 +2013,6 @@ ctx.globalAlpha = 1; } - // 绘制背景图(高清版本) - function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scaleX, scaleY) { - // 使用原始图片尺寸进行高清渲染 - const containerWidth = canvasWidth; - const containerHeight = canvasHeight; - 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; - - // 计算位置(考虑高清缩放) - const baseLeft = (containerWidth - scaledWidth) / 2; - const drawX = baseLeft + (imageOffsetX * scaleX); - const drawY = (containerHeight - scaledHeight) / 2; - - ctx.globalAlpha = bgImageOpacity; - // 使用原始图片绘制,获得最佳清晰度 - ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); - ctx.globalAlpha = 1; - } - // 绘制层叠图 function drawOverlayImages(ctx) { return new Promise((resolve) => { @@ -2227,61 +2059,6 @@ }); } - // 绘制层叠图(高清版本) - function drawOverlayImagesHD(ctx, scaleX, scaleY) { - 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 * scaleX; - const top = overlayEl.offsetTop * scaleY; - const width = overlayEl.offsetWidth * scaleX; - const height = overlayEl.offsetHeight * scaleY; - - 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'); @@ -2311,35 +2088,6 @@ ctx.fillText(text, inputLeft, inputTop + textOffsetY); } - // 在画布上绘制文字(高清版本) - function drawTextOnCanvasHD(ctx, inputElement, scaleX, scaleY) { - 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 * scaleX; - const inputTop = inputElement.offsetTop * scaleY; - - const style = window.getComputedStyle(inputElement); - const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放 - const lineHeight = parseFloat(style.lineHeight); - - // 使用高清字体渲染 - ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`; - ctx.fillStyle = style.color; - ctx.textBaseline = 'alphabetic'; - - // 计算文本垂直偏移量,使其与 DOM 渲染一致 - const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY; - const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; - - ctx.fillText(text, inputLeft, inputTop + textOffsetY); - } - // 保存图片 async function saveImage() { if (!currentImage) { @@ -2349,7 +2097,7 @@ try { const canvas = await generateCanvas(); - await showMobileSaveTip(canvas); + showMobileSaveTip(); } catch (err) { console.error('保存失败:', err); alert('保存失败,请重试'); @@ -2357,42 +2105,10 @@ } // 显示移动端保存选项 - async function showMobileSaveTip(canvas) { - console.log('导出画布尺寸:', canvas.width, 'x', canvas.height); - - // 在 toBlob 之前再次确认 canvas 尺寸 - console.log('toBlob 前 canvas 尺寸:', 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'); + function showMobileSaveTip() { + const canvas = exportCanvas; + const dataUrl = canvas.toDataURL('image/png'); const filename = '邀请函_' + Date.now() + '.png'; - - // 保存 blob 供后续使用 - window._lastExportBlob = blob; const tipOverlay = document.createElement('div'); tipOverlay.id = 'mobileSaveTip'; @@ -2443,7 +2159,7 @@ `; saveBtn.onclick = async () => { tipOverlay.remove(); - await saveImageToGallery(dataUrl, blob); + await saveImageToGallery(dataUrl); }; const cancelBtn = document.createElement('button'); @@ -2458,23 +2174,11 @@ border-radius: 10px; cursor: pointer; `; - cancelBtn.onclick = () => { - tipOverlay.remove(); - // 重新隐藏 canvas - exportCanvas.setAttribute('hidden', ''); - exportCanvas.style.position = ''; - exportCanvas.style.left = ''; - exportCanvas.style.top = ''; - }; + cancelBtn.onclick = () => tipOverlay.remove(); tipOverlay.addEventListener('click', (e) => { if (e.target === tipOverlay) { tipOverlay.remove(); - // 重新隐藏 canvas - exportCanvas.setAttribute('hidden', ''); - exportCanvas.style.position = ''; - exportCanvas.style.left = ''; - exportCanvas.style.top = ''; } }); @@ -2500,7 +2204,7 @@ `; previewBtn.onclick = () => { tipOverlay.remove(); - showPreviewFromBlob(blob); + showPreview(); }; // 插入到标题后面