diff --git a/README.md b/README.md index 61f35c3..8bb3c6f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ 20. 当选择文字的时候工具栏对应出现文字编辑器,当选择图片的时候工具栏对应出现图片编辑器,点击其他位置时隐藏工具栏,两个编辑栏的宽高保持和文本编辑器工具栏一致 21. 调整图文工具栏布局,解决顶部遮挡问题,图片透明度滑块添加百分比 22. 给背景图添加编辑器,与层叠图编辑器文本编辑器通用一个工具栏,背景图工具栏功能为:背景图可以放大缩小,可以调节透明度,其余功能不受影响,点击层叠图只显示当前编辑器工具栏 - +23. 校准画布编辑与图片导出文字垂直位置偏移问题,修改屏幕尺寸为960x1361像素 diff --git a/css/style.css b/css/style.css index d1f6570..9fb2945 100644 --- a/css/style.css +++ b/css/style.css @@ -261,7 +261,7 @@ html, body { position: relative; width: 100%; max-width: 360px; - aspect-ratio: 3 / 5; + aspect-ratio: 960 / 1361; box-shadow: 0 4px 20px rgba(0,0,0,0.15); background: #fff; overflow: hidden; @@ -372,36 +372,34 @@ html, body { position: absolute; min-width: 80px; min-height: 32px; - padding: 4px 8px; + padding: 0; font-size: 16px; color: #000; font-family: "Microsoft YaHei", "微软雅黑", sans-serif; background: transparent; - border: 2px solid transparent; + outline: 2px solid transparent; border-radius: 4px; - outline: none; cursor: grab; white-space: nowrap; line-height: 1.4; -webkit-user-select: none; user-select: none; - transition: border-color 0.2s; + transition: outline-color 0.2s; z-index: 10; - /* 确保输入框内容超出时不影响父容器和其他元素 */ overflow: visible; max-width: none; box-sizing: border-box; } -/* 选中状态(单击/长按) - 仅显示虚线边框 */ +/* 选中状态(单击/长按) - 仅显示虚线轮廓 */ .text-input.selected { - border: 2px dashed rgba(74, 144, 226, 0.6); + outline: 2px dashed rgba(74, 144, 226, 0.6); cursor: grab; } -/* 编辑状态(双击) - 显示实线边框 */ +/* 编辑状态(双击) - 显示实线轮廓 */ .text-input.editing { - border: 2px solid #4a90e2; + outline: 2px solid #4a90e2; background: rgba(255,255,255,0.1); cursor: text; -webkit-user-select: text; diff --git a/js/script.js b/js/script.js index 2695983..35fb212 100644 --- a/js/script.js +++ b/js/script.js @@ -864,8 +864,10 @@ startX = clientX; startY = clientY; - startLeft = element.offsetLeft; - startTop = element.offsetTop; + const containerRect = imageContainer.getBoundingClientRect(); + const elemRect = element.getBoundingClientRect(); + startLeft = elemRect.left - containerRect.left; + startTop = elemRect.top - containerRect.top; // 延迟判断是否为拖拽 dragTimer = setTimeout(() => { @@ -1782,31 +1784,27 @@ } } - // 生成画布(预览与保存样式一致) + // 生成画布(所见即所得 - 使用显示尺寸) async function generateCanvas() { const canvas = exportCanvas; const ctx = canvas.getContext('2d'); const container = imageContainer; - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; + const width = container.clientWidth; + const height = container.clientHeight; - canvas.width = containerWidth; - canvas.height = containerHeight; + canvas.width = width; + canvas.height = height; - // 绘制白色背景 ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillRect(0, 0, width, height); - // 绘制背景图(考虑滑动偏移) if (currentImage) { - drawImageWithOffset(ctx, currentImage, containerWidth, containerHeight); + drawBgImageOnCanvas(ctx, currentImage, width, height); } - // 绘制层叠图(等待图片加载完成) await drawOverlayImages(ctx); - // 绘制文字(动态获取所有文本输入框) const textInputs = container.querySelectorAll('.preview-input-layer .text-input'); textInputs.forEach(textInput => { drawTextOnCanvas(ctx, textInput); @@ -1815,6 +1813,38 @@ 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 drawOverlayImages(ctx) { return new Promise((resolve) => { @@ -1832,20 +1862,25 @@ 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(); @@ -1856,42 +1891,33 @@ }); } - // 绘制图片(考虑滑动偏移) - 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 deleteBtn = inputElement.querySelector('.text-delete-btn'); + let text = inputElement.textContent; + if (deleteBtn) { + text = text.replace(deleteBtn.textContent, ''); + } + if (!text.trim()) return; - const style = window.getComputedStyle(inputElement); + // 使用 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 = 'top'; + ctx.textBaseline = 'alphabetic'; - ctx.fillText(text, inputLeft, inputTop); + // 计算文本垂直偏移量,使其与 DOM 渲染一致 + // DOM 中 line-height 会影响文本位置,需要计算实际文本基线位置 + const actualLineHeight = lineHeight || fontSize * 1.4; + const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; + + ctx.fillText(text, inputLeft, inputTop + textOffsetY); } // 保存图片