From e771988338384b84a772b7654dc752f264eb0d9f Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:05:33 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat(canvas):=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=B5=8F=E8=A7=88=E5=99=A8=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=AF=BC=E5=87=BA=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 146 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/js/script.js b/js/script.js index 4fc97d6..8044d30 100644 --- a/js/script.js +++ b/js/script.js @@ -2006,21 +2006,46 @@ } } - // 生成画布(高清无损,固定 1080×1920 分辨率) + // 检测是否在微信内置浏览器 + function isWechat() { + return /MicroMessenger/i.test(navigator.userAgent); + } + + // 生成画布(高清无损,根据设备像素比自适应分辨率) 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); - - // 显示尺寸(用于计算缩放比例) + // 根据设备像素比决定输出分辨率,移动端至少 2x + // 微信等浏览器 canvas 最大尺寸通常为 4096,所以上限设为 2160×3840 + const dpr = window.devicePixelRatio || 1; + const minScale = Math.max(dpr, 2); // 至少 2x const displayWidth = container.clientWidth; const displayHeight = container.clientHeight; + + let TARGET_WIDTH = Math.round(displayWidth * minScale); + let TARGET_HEIGHT = Math.round(displayHeight * minScale); + + // 微信特殊处理:微信内置浏览器 canvas 限制更严格 + // 实测微信中 4096 可能不稳定,使用 2048 更安全 + const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096; + if (TARGET_HEIGHT > MAX_CANVAS_SIZE) { + const ratio = MAX_CANVAS_SIZE / TARGET_HEIGHT; + TARGET_WIDTH = Math.round(TARGET_WIDTH * ratio); + TARGET_HEIGHT = MAX_CANVAS_SIZE; + } + if (TARGET_WIDTH > MAX_CANVAS_SIZE) { + const ratio = MAX_CANVAS_SIZE / TARGET_WIDTH; + TARGET_HEIGHT = Math.round(TARGET_HEIGHT * ratio); + TARGET_WIDTH = MAX_CANVAS_SIZE; + } + + // 确保最小分辨率 + TARGET_WIDTH = Math.max(TARGET_WIDTH, 1080); + TARGET_HEIGHT = Math.max(TARGET_HEIGHT, 1920); + + console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat()); console.log('显示容器尺寸:', displayWidth, 'x', displayHeight); // 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制 @@ -2122,8 +2147,36 @@ const drawY = (containerHeight - scaledHeight) / 2; ctx.globalAlpha = bgImageOpacity; - // 使用原始图片绘制,获得最佳清晰度 - ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); + + // 微信中:如果原始图片比目标区域大,使用原始尺寸绘制以获得最佳质量 + // 否则按计算尺寸绘制 + if (isWechat() && img.naturalWidth > scaledWidth && img.naturalHeight > scaledHeight) { + // 计算裁剪区域,从原始图片中心裁剪 + const srcRatio = scaledWidth / scaledHeight; + let srcWidth = img.naturalWidth; + let srcHeight = img.naturalHeight; + let srcX = 0; + let srcY = 0; + + // 如果原始图片比例与目标不同,计算居中裁剪 + const naturalRatio = img.naturalWidth / img.naturalHeight; + if (naturalRatio > imgRatio) { + // 原始图片更宽,裁剪左右 + srcWidth = img.naturalHeight * imgRatio; + srcX = (img.naturalWidth - srcWidth) / 2; + } else if (naturalRatio < imgRatio) { + // 原始图片更高,裁剪上下 + srcHeight = img.naturalWidth / imgRatio; + srcY = (img.naturalHeight - srcHeight) / 2; + } + + // 使用 drawImage 的 9 参数版本进行高质量缩放 + ctx.drawImage(img, srcX, srcY, srcWidth, srcHeight, drawX, drawY, scaledWidth, scaledHeight); + } else { + // 使用原始图片绘制,获得最佳清晰度 + ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); + } + ctx.globalAlpha = 1; } @@ -2274,10 +2327,21 @@ const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放 const lineHeight = parseFloat(style.lineHeight); + // 微信中:使用更粗的字体权重以获得更好的可读性 + let fontWeight = style.fontWeight; + if (isWechat() && fontWeight === '400' || fontWeight === 'normal') { + fontWeight = '500'; // 稍微加粗一点 + } + // 使用高清字体渲染 - ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`; + ctx.font = `${fontWeight} ${fontSize}px ${style.fontFamily}`; ctx.fillStyle = style.color; ctx.textBaseline = 'alphabetic'; + + // 微信中:开启文字抗锯齿 + if (isWechat()) { + ctx.textRendering = 'optimizeLegibility'; + } // 计算文本垂直偏移量,使其与 DOM 渲染一致 const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY; @@ -2309,29 +2373,49 @@ // 在 toBlob 之前再次确认 canvas 尺寸 console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height); - // 尝试使用 toDataURL 获取更高质量的图片 + // 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损 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); + // 微信中优先使用 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'); From 84d51d30c83141bd015c7de7bfb4f76898fec14b Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:10:43 +0800 Subject: [PATCH 02/19] =?UTF-8?q?fix(canvas):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=BC=A9=E6=94=BE=E6=AF=94=E4=BE=8B=E4=BB=A5=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E5=AE=BD=E9=AB=98=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 79 +++++++++++++++++----------------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/js/script.js b/js/script.js index 9de4ac4..1ce3c82 100644 --- a/js/script.js +++ b/js/script.js @@ -2122,20 +2122,20 @@ ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT); // 计算从显示尺寸到目标分辨率的缩放比例 - const scaleX = TARGET_WIDTH / displayWidth; - const scaleY = TARGET_HEIGHT / displayHeight; - console.log('缩放比例:', scaleX, 'x', scaleY); + // 使用统一的缩放比例,保持宽高比,确保所见即所得 + const scale = TARGET_WIDTH / displayWidth; + console.log('统一缩放比例:', scale); if (currentImage) { console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight); - drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scaleX, scaleY); + drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scale); } - await drawOverlayImagesHD(ctx, scaleX, scaleY); + await drawOverlayImagesHD(ctx, scale); const textInputs = container.querySelectorAll('.preview-input-layer .text-input'); textInputs.forEach(textInput => { - drawTextOnCanvasHD(ctx, textInput, scaleX, scaleY); + drawTextOnCanvasHD(ctx, textInput, scale); }); return canvas; @@ -2174,8 +2174,8 @@ } // 绘制背景图(高清版本) - function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scaleX, scaleY) { - // 使用原始图片尺寸进行高清渲染 + function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scale) { + // 使用与编辑时完全相同的计算逻辑,只是按比例缩放 const containerWidth = canvasWidth; const containerHeight = canvasHeight; const imgRatio = img.naturalWidth / img.naturalHeight; @@ -2191,46 +2191,17 @@ imgHeight = imgWidth / imgRatio; } - // 应用缩放 + // 应用缩放(与编辑时的 bgImageScale 一致) const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; - // 计算位置(考虑高清缩放) + // 计算位置(与 updateBgImage 函数完全一致,只是按比例缩放偏移量) const baseLeft = (containerWidth - scaledWidth) / 2; - const drawX = baseLeft + (imageOffsetX * scaleX); + const drawX = baseLeft + (imageOffsetX * scale); const drawY = (containerHeight - scaledHeight) / 2; ctx.globalAlpha = bgImageOpacity; - - // 微信中:如果原始图片比目标区域大,使用原始尺寸绘制以获得最佳质量 - // 否则按计算尺寸绘制 - if (isWechat() && img.naturalWidth > scaledWidth && img.naturalHeight > scaledHeight) { - // 计算裁剪区域,从原始图片中心裁剪 - const srcRatio = scaledWidth / scaledHeight; - let srcWidth = img.naturalWidth; - let srcHeight = img.naturalHeight; - let srcX = 0; - let srcY = 0; - - // 如果原始图片比例与目标不同,计算居中裁剪 - const naturalRatio = img.naturalWidth / img.naturalHeight; - if (naturalRatio > imgRatio) { - // 原始图片更宽,裁剪左右 - srcWidth = img.naturalHeight * imgRatio; - srcX = (img.naturalWidth - srcWidth) / 2; - } else if (naturalRatio < imgRatio) { - // 原始图片更高,裁剪上下 - srcHeight = img.naturalWidth / imgRatio; - srcY = (img.naturalHeight - srcHeight) / 2; - } - - // 使用 drawImage 的 9 参数版本进行高质量缩放 - ctx.drawImage(img, srcX, srcY, srcWidth, srcHeight, drawX, drawY, scaledWidth, scaledHeight); - } else { - // 使用原始图片绘制,获得最佳清晰度 - ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); - } - + ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); ctx.globalAlpha = 1; } @@ -2281,7 +2252,7 @@ } // 绘制层叠图(高清版本) - function drawOverlayImagesHD(ctx, scaleX, scaleY) { + function drawOverlayImagesHD(ctx, scale) { return new Promise((resolve) => { const overlayImages = overlayLayer.querySelectorAll('.overlay-image'); if (overlayImages.length === 0) { @@ -2306,11 +2277,11 @@ return; } - // 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置,并应用高清缩放 - const left = overlayEl.offsetLeft * scaleX; - const top = overlayEl.offsetTop * scaleY; - const width = overlayEl.offsetWidth * scaleX; - const height = overlayEl.offsetHeight * scaleY; + // 使用 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; @@ -2365,7 +2336,7 @@ } // 在画布上绘制文字(高清版本) - function drawTextOnCanvasHD(ctx, inputElement, scaleX, scaleY) { + function drawTextOnCanvasHD(ctx, inputElement, scale) { const deleteBtn = inputElement.querySelector('.text-delete-btn'); let text = inputElement.textContent; if (deleteBtn) { @@ -2373,17 +2344,17 @@ } if (!text.trim()) return; - // 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置,并应用高清缩放 - const inputLeft = inputElement.offsetLeft * scaleX; - const inputTop = inputElement.offsetTop * scaleY; + // 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置,并应用统一缩放 + const inputLeft = inputElement.offsetLeft * scale; + const inputTop = inputElement.offsetTop * scale; const style = window.getComputedStyle(inputElement); - const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放 + const fontSize = parseInt(style.fontSize) * scale; // 字体大小也按比例缩放 const lineHeight = parseFloat(style.lineHeight); // 微信中:使用更粗的字体权重以获得更好的可读性 let fontWeight = style.fontWeight; - if (isWechat() && fontWeight === '400' || fontWeight === 'normal') { + if (isWechat() && (fontWeight === '400' || fontWeight === 'normal')) { fontWeight = '500'; // 稍微加粗一点 } @@ -2398,7 +2369,7 @@ } // 计算文本垂直偏移量,使其与 DOM 渲染一致 - const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY; + const actualLineHeight = (lineHeight || fontSize * 1.4); const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; ctx.fillText(text, inputLeft, inputTop + textOffsetY); From 50379cc4b19d70f0eec22d44f5fc96ef354b4295 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:14:32 +0800 Subject: [PATCH 03/19] =?UTF-8?q?refactor(canvas):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=B0=BA=E5=AF=B8=E8=AE=A1=E7=AE=97=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A5=E4=BF=9D=E6=8C=81=E5=AE=BD=E9=AB=98=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 51 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/js/script.js b/js/script.js index 1ce3c82..845e75b 100644 --- a/js/script.js +++ b/js/script.js @@ -2065,39 +2065,54 @@ return /MicroMessenger/i.test(navigator.userAgent); } - // 生成画布(高清无损,根据设备像素比自适应分辨率) + // 生成画布(高清无损,保持与显示容器相同的宽高比) async function generateCanvas() { const canvas = exportCanvas; const ctx = canvas.getContext('2d'); const container = imageContainer; - // 根据设备像素比决定输出分辨率,移动端至少 2x - // 微信等浏览器 canvas 最大尺寸通常为 4096,所以上限设为 2160×3840 - const dpr = window.devicePixelRatio || 1; - const minScale = Math.max(dpr, 2); // 至少 2x + // 获取显示容器的实际尺寸和宽高比 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 限制更严格 - // 实测微信中 4096 可能不稳定,使用 2048 更安全 const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096; - if (TARGET_HEIGHT > MAX_CANVAS_SIZE) { - const ratio = MAX_CANVAS_SIZE / TARGET_HEIGHT; - TARGET_WIDTH = Math.round(TARGET_WIDTH * ratio); - TARGET_HEIGHT = MAX_CANVAS_SIZE; - } - if (TARGET_WIDTH > MAX_CANVAS_SIZE) { - const ratio = MAX_CANVAS_SIZE / TARGET_WIDTH; - TARGET_HEIGHT = Math.round(TARGET_HEIGHT * ratio); - TARGET_WIDTH = MAX_CANVAS_SIZE; + + // 如果超出限制,按宽高比等比例缩放 + 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); } - // 确保最小分辨率 - TARGET_WIDTH = Math.max(TARGET_WIDTH, 1080); - TARGET_HEIGHT = Math.max(TARGET_HEIGHT, 1920); + // 确保最小分辨率,同时保持宽高比 + 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); From 4e7b7734a7aad0e45164b0a3354270debeb94431 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:16:56 +0800 Subject: [PATCH 04/19] =?UTF-8?q?fix(wechat):=20=E4=BF=AE=E5=A4=8D=20canva?= =?UTF-8?q?s=20=E5=B0=BA=E5=AF=B8=E8=AE=BE=E7=BD=AE=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=8F=8A=E4=B8=8A=E4=B8=8B=E6=96=87=E8=8E=B7=E5=8F=96=E6=97=B6?= =?UTF-8?q?=E6=9C=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/js/script.js b/js/script.js index 845e75b..4ba2ee6 100644 --- a/js/script.js +++ b/js/script.js @@ -2068,7 +2068,6 @@ // 生成画布(高清无损,保持与显示容器相同的宽高比) async function generateCanvas() { const canvas = exportCanvas; - const ctx = canvas.getContext('2d'); const container = imageContainer; // 获取显示容器的实际尺寸和宽高比 @@ -2085,6 +2084,7 @@ let TARGET_HEIGHT = Math.round(displayHeight * minScale); // 微信特殊处理:微信内置浏览器 canvas 限制更严格 + // 实测微信中 canvas 最大尺寸约为 4096,但超过 2048 可能有问题 const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096; // 如果超出限制,按宽高比等比例缩放 @@ -2126,7 +2126,18 @@ // 设置画布为高清分辨率 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; @@ -2139,7 +2150,7 @@ // 计算从显示尺寸到目标分辨率的缩放比例 // 使用统一的缩放比例,保持宽高比,确保所见即所得 const scale = TARGET_WIDTH / displayWidth; - console.log('统一缩放比例:', scale); + console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio); if (currentImage) { console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight); From 332bfc9b45d7bcd09982768546b20b9a71ceab22 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:23:47 +0800 Subject: [PATCH 05/19] =?UTF-8?q?refactor(drag):=20=E9=99=90=E5=88=B6?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E4=B8=8D=E5=BC=BA=E5=88=B6=E5=90=B8=E9=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/js/script.js b/js/script.js index 4ba2ee6..994af7e 100644 --- a/js/script.js +++ b/js/script.js @@ -458,15 +458,13 @@ let newLeft = startLeft + dx; let newTop = startTop + dy; - // 使用吸附功能 - const snapped = getSnapPosition(overlayEl, newLeft, newTop); + // 不启用吸附,只限制边界,但显示辅助线 + const snapped = getSnapPosition(overlayEl, newLeft, newTop, false); newLeft = snapped.left; newTop = snapped.top; - - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) - if (snapped.showGuide) { - showAlignmentGuides(overlayEl); - } + + // 显示对齐辅助线 + showAlignmentGuides(overlayEl); overlayEl.style.left = newLeft + 'px'; overlayEl.style.top = newTop + 'px'; @@ -993,15 +991,13 @@ const newLeft = startLeft + dx; const newTop = startTop + dy; - // 检测对齐并吸附 - const snapped = getSnapPosition(element, newLeft, newTop); + // 不启用吸附,只限制边界,但显示辅助线 + const snapped = getSnapPosition(element, newLeft, newTop, false); element.style.left = snapped.left + 'px'; element.style.top = snapped.top + 'px'; - - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) - if (snapped.showGuide) { - showAlignmentGuides(element); - } + + // 显示对齐辅助线 + showAlignmentGuides(element); } // 阻止事件冒泡,避免触发图片滑动 @@ -1194,8 +1190,8 @@ } } - // 获取吸附后的位置 - function getSnapPosition(draggedEl, newLeft, newTop) { + // 获取吸附后的位置(可选吸附,不强制) + function getSnapPosition(draggedEl, newLeft, newTop, enableSnap = false) { const container = imageContainer; const containerRect = container.getBoundingClientRect(); @@ -1216,11 +1212,21 @@ const cCenterX = cWidth / 2; const cCenterY = cHeight / 2; - const threshold = 1; + // 默认返回原始位置(不吸附) 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'); @@ -1384,7 +1390,7 @@ } } - // 应用吸附 + // 应用吸附(仅在启用吸附时) if (snapX && snapXPos !== null) { snapLeft = snapXPos; showGuide = true; @@ -1780,15 +1786,9 @@ return; } - // 应用新位置 + // 应用新位置(不启用吸附) targetEl.style.left = currentLeft + 'px'; targetEl.style.top = currentTop + 'px'; - - // 显示对齐辅助线(网格线已在 getSnapPosition 中显示) - const snapped = getSnapPosition(targetEl, currentLeft, currentTop); - if (snapped.showGuide) { - showAlignmentGuides(targetEl); - } }); } From dfaff637af328ec896ede76587f3453dadaff2fb Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:26:58 +0800 Subject: [PATCH 06/19] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=BC=96=E8=BE=91=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/js/script.js b/js/script.js index 994af7e..6f1fb5c 100644 --- a/js/script.js +++ b/js/script.js @@ -1034,7 +1034,7 @@ } } - // 双击进入编辑模式(电脑端和移动端通用) + // 双击进入编辑模式(电脑端) element.addEventListener('dblclick', (e) => { e.stopPropagation(); startEdit(element); @@ -1042,20 +1042,45 @@ setTimeout(() => { element.dataset.justEdited = 'false'; }, 200); }); - // 单击选中显示删除按钮 + // 单击选中并进入编辑模式(移动端友好) element.addEventListener('click', (e) => { if (hasMoved) return; if (element.dataset.justEdited === 'true') return; - element.classList.add('selected'); + + // 如果已经在编辑中,不重复进入 + if (editingInput === element) return; + + // 选中并立即进入编辑模式 + selectInput(element); + startEdit(element); }); - // 触摸双击检测(移动端双击进入编辑) + // 移动端:触摸结束后进入编辑(更灵敏) let lastTapTime = 0; + let touchEditTriggered = false; element.addEventListener('touchend', (e) => { + // 如果正在拖拽,不触发编辑 + if (isDragging) return; + + // 防止 touchend 后立即触发 click 导致重复进入编辑 + e.preventDefault(); + e.stopPropagation(); + const currentTime = Date.now(); + // 双击检测(300ms内两次点击) if (currentTime - lastTapTime < 300) { - e.stopPropagation(); - startEdit(element); + // 双击已在 click 事件中处理,这里不重复 + touchEditTriggered = true; + } else { + // 单击延迟处理,避免与拖拽冲突 + touchEditTriggered = false; + setTimeout(() => { + if (!isDragging && !touchEditTriggered && editingInput !== element) { + touchEditTriggered = true; + selectInput(element); + startEdit(element); + } + }, 50); } lastTapTime = currentTime; }); From bc37358027a90ffe9cd46e080fdd8c73b01589d9 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:28:51 +0800 Subject: [PATCH 07/19] =?UTF-8?q?fix(ios):=20=E8=A7=A3=E5=86=B3=20iOS=20?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E4=B8=8A=E9=94=AE=E7=9B=98=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=BC=B9=E5=87=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/js/script.js b/js/script.js index 6f1fb5c..7d96b31 100644 --- a/js/script.js +++ b/js/script.js @@ -1148,8 +1148,28 @@ sel.addRange(range); } element.focus(); + + // 尝试多种方式触发键盘弹出 + // 方式1: 标准 focus + element.focus(); + + // 方式2: 对于 iOS,需要创建一个临时的 input 来触发键盘 + if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { + const tempInput = document.createElement('input'); + tempInput.style.position = 'fixed'; + tempInput.style.opacity = '0'; + tempInput.style.top = '-1000px'; + document.body.appendChild(tempInput); + tempInput.focus(); + setTimeout(() => { + tempInput.blur(); + tempInput.remove(); + element.focus(); + }, 100); + } }, 10); + // 方式3: 使用 Virtual Keyboard API(如果支持) if ('virtualKeyboard' in navigator) { navigator.virtualKeyboard.show(); } From 6506988f33029d1b83c5d11e0068b8d2924d5ebf Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:30:31 +0800 Subject: [PATCH 08/19] =?UTF-8?q?fix(mobile):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=A7=A6=E6=91=B8=E7=BC=96=E8=BE=91=E5=92=8C=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/js/script.js b/js/script.js index 7d96b31..74cb860 100644 --- a/js/script.js +++ b/js/script.js @@ -1057,26 +1057,18 @@ // 移动端:触摸结束后进入编辑(更灵敏) let lastTapTime = 0; - let touchEditTriggered = false; element.addEventListener('touchend', (e) => { // 如果正在拖拽,不触发编辑 if (isDragging) return; - // 防止 touchend 后立即触发 click 导致重复进入编辑 - e.preventDefault(); - e.stopPropagation(); - const currentTime = Date.now(); // 双击检测(300ms内两次点击) if (currentTime - lastTapTime < 300) { // 双击已在 click 事件中处理,这里不重复 - touchEditTriggered = true; } else { // 单击延迟处理,避免与拖拽冲突 - touchEditTriggered = false; setTimeout(() => { - if (!isDragging && !touchEditTriggered && editingInput !== element) { - touchEditTriggered = true; + if (!isDragging && editingInput !== element) { selectInput(element); startEdit(element); } @@ -1137,6 +1129,9 @@ // 更新工具栏状态 updateToolbarState(element); + // 立即 focus 以触发键盘(必须在用户交互的同步上下文中) + element.focus(); + // 如果没有选中文本,则定位光标到末尾 setTimeout(() => { const sel = window.getSelection(); @@ -1147,29 +1142,9 @@ sel.removeAllRanges(); sel.addRange(range); } - element.focus(); - - // 尝试多种方式触发键盘弹出 - // 方式1: 标准 focus - element.focus(); - - // 方式2: 对于 iOS,需要创建一个临时的 input 来触发键盘 - if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { - const tempInput = document.createElement('input'); - tempInput.style.position = 'fixed'; - tempInput.style.opacity = '0'; - tempInput.style.top = '-1000px'; - document.body.appendChild(tempInput); - tempInput.focus(); - setTimeout(() => { - tempInput.blur(); - tempInput.remove(); - element.focus(); - }, 100); - } - }, 10); + }, 0); - // 方式3: 使用 Virtual Keyboard API(如果支持) + // 使用 Virtual Keyboard API(如果支持) if ('virtualKeyboard' in navigator) { navigator.virtualKeyboard.show(); } From 53553ab90b05226ca2a6fd1f9bafb52829fb147e Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:32:37 +0800 Subject: [PATCH 09/19] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E5=92=8C=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/js/script.js b/js/script.js index 74cb860..b20e623 100644 --- a/js/script.js +++ b/js/script.js @@ -1042,7 +1042,7 @@ setTimeout(() => { element.dataset.justEdited = 'false'; }, 200); }); - // 单击选中并进入编辑模式(移动端友好) + // 单击进入编辑模式(同时支持桌面和移动端) element.addEventListener('click', (e) => { if (hasMoved) return; if (element.dataset.justEdited === 'true') return; @@ -1054,28 +1054,6 @@ selectInput(element); startEdit(element); }); - - // 移动端:触摸结束后进入编辑(更灵敏) - let lastTapTime = 0; - element.addEventListener('touchend', (e) => { - // 如果正在拖拽,不触发编辑 - if (isDragging) return; - - const currentTime = Date.now(); - // 双击检测(300ms内两次点击) - if (currentTime - lastTapTime < 300) { - // 双击已在 click 事件中处理,这里不重复 - } else { - // 单击延迟处理,避免与拖拽冲突 - setTimeout(() => { - if (!isDragging && editingInput !== element) { - selectInput(element); - startEdit(element); - } - }, 50); - } - lastTapTime = currentTime; - }); } // 选中输入框 From 6057088b609f816860b486bffa5886106520bf29 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:42:04 +0800 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E9=AB=98?= =?UTF-8?q?=E6=B8=85=E5=AF=BC=E5=87=BA=E6=97=B6=E8=83=8C=E6=99=AF=E5=9B=BE?= =?UTF-8?q?=E5=92=8C=E6=96=87=E6=9C=AC=E7=9A=84=E7=BC=A9=E6=94=BE=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 62 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/js/script.js b/js/script.js index b20e623..28ea185 100644 --- a/js/script.js +++ b/js/script.js @@ -2147,6 +2147,7 @@ // 计算从显示尺寸到目标分辨率的缩放比例 // 使用统一的缩放比例,保持宽高比,确保所见即所得 + // 注意:必须在 canvas 尺寸验证之后计算 scale const scale = TARGET_WIDTH / displayWidth; console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio); @@ -2199,19 +2200,20 @@ // 绘制背景图(高清版本) function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scale) { - // 使用与编辑时完全相同的计算逻辑,只是按比例缩放 - const containerWidth = canvasWidth; - const containerHeight = canvasHeight; + // 先按显示容器的尺寸计算(与 updateBgImage 完全一致) + const displayContainer = imageContainer; + const displayWidth = displayContainer.clientWidth; + const displayHeight = displayContainer.clientHeight; const imgRatio = img.naturalWidth / img.naturalHeight; - const containerRatio = containerWidth / containerHeight; + const displayRatio = displayWidth / displayHeight; let imgWidth, imgHeight; - if (imgRatio > containerRatio) { - imgHeight = containerHeight; + if (imgRatio > displayRatio) { + imgHeight = displayHeight; imgWidth = imgHeight * imgRatio; } else { - imgWidth = containerWidth; + imgWidth = displayWidth; imgHeight = imgWidth / imgRatio; } @@ -2219,13 +2221,32 @@ const scaledWidth = imgWidth * bgImageScale; const scaledHeight = imgHeight * bgImageScale; - // 计算位置(与 updateBgImage 函数完全一致,只是按比例缩放偏移量) - const baseLeft = (containerWidth - scaledWidth) / 2; - const drawX = baseLeft + (imageOffsetX * scale); - const drawY = (containerHeight - scaledHeight) / 2; + // 计算位置(与 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; - ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight); + + // 调试信息 + 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; } @@ -2373,8 +2394,18 @@ const inputTop = inputElement.offsetTop * scale; const style = window.getComputedStyle(inputElement); - const fontSize = parseInt(style.fontSize) * scale; // 字体大小也按比例缩放 - const lineHeight = parseFloat(style.lineHeight); + 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; @@ -2393,7 +2424,8 @@ } // 计算文本垂直偏移量,使其与 DOM 渲染一致 - const actualLineHeight = (lineHeight || fontSize * 1.4); + // 使用缩放后的 lineHeight 计算 + const actualLineHeight = lineHeight || fontSize * 1.4; const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85; ctx.fillText(text, inputLeft, inputTop + textOffsetY); From b881f3192001971d68574c618e0e75b162338bf6 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:48:14 +0800 Subject: [PATCH 11/19] =?UTF-8?q?fix(mobile):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E5=9B=BA=E5=AE=9A=E4=B8=8E=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/style.css | 9 +++++++++ js/script.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/css/style.css b/css/style.css index e49d0a1..c957ceb 100644 --- a/css/style.css +++ b/css/style.css @@ -104,6 +104,15 @@ html, body { z-index: 100; } +/* 编辑模式:粘在视口顶部始终可见 */ +.editor-toolbar.editing-mode { + position: fixed; + top: 0; + border-bottom: 1px solid #e0e0e0; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + z-index: 10000; +} + .editor-toolbar.active { visibility: visible; opacity: 1; diff --git a/js/script.js b/js/script.js index 28ea185..b4d7527 100644 --- a/js/script.js +++ b/js/script.js @@ -197,7 +197,18 @@ } imageContainer.addEventListener('click', handleContainerClick); - imageContainer.addEventListener('touchend', handleContainerClick); + // 移动端:touchend 时如果正在编辑文本,不处理容器点击 + imageContainer.addEventListener('touchend', (e) => { + // 如果触摸目标是文本输入框或正在编辑,不触发容器点击 + if (e.target.closest('.text-input')) { + return; + } + // 如果正在编辑文本,不触发容器点击(避免隐藏工具栏) + if (editingInput) { + return; + } + handleContainerClick(e); + }); // 初始化长按保存(只在3:5预览区域) initLongPressSave(); @@ -1104,6 +1115,20 @@ 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); @@ -1134,6 +1159,9 @@ 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'))) From 91463b31a3d49c5bf313c530fc0b637ed4965a61 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:52:01 +0800 Subject: [PATCH 12/19] =?UTF-8?q?fix(toolbar):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=9C=A8=E9=80=89=E4=B8=AD=E5=85=83=E7=B4=A0=E4=B8=8A=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/js/script.js b/js/script.js index b4d7527..8ca1bcd 100644 --- a/js/script.js +++ b/js/script.js @@ -1680,16 +1680,17 @@ function initTextEditorToolbar() { // 加粗按钮 boldBtn.addEventListener('click', () => { - if (!editingInput) return; + 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) { - editingInput.style.fontWeight = 'normal'; + targetInput.style.fontWeight = 'normal'; } else { - editingInput.style.fontWeight = 'bold'; + targetInput.style.fontWeight = 'bold'; } boldBtn.classList.toggle('active'); } @@ -1697,16 +1698,17 @@ // 斜体按钮 italicBtn.addEventListener('click', () => { - if (!editingInput) return; + 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) { - editingInput.style.fontStyle = 'normal'; + targetInput.style.fontStyle = 'normal'; } else { - editingInput.style.fontStyle = 'italic'; + targetInput.style.fontStyle = 'italic'; } italicBtn.classList.toggle('active'); } @@ -1714,19 +1716,21 @@ // 颜色选择器 colorPicker.addEventListener('input', () => { - if (!editingInput) return; + const targetInput = editingInput || selectedInput; + if (!targetInput) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { document.execCommand('foreColor', false, colorPicker.value); } else { - editingInput.style.color = colorPicker.value; + targetInput.style.color = colorPicker.value; } }); // 字体选择器 fontPicker.addEventListener('change', () => { - if (!editingInput) return; - editingInput.style.fontFamily = fontPicker.value + ', sans-serif'; + const targetInput = editingInput || selectedInput; + if (!targetInput) return; + targetInput.style.fontFamily = fontPicker.value + ', sans-serif'; }); // 字号选择器 - 密码锁式滚轮 From 909724ced035885aaf0b777e59ece66e26ae35a9 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 09:57:32 +0800 Subject: [PATCH 13/19] =?UTF-8?q?fix(canvas):=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E5=8A=A0=E8=BD=BD=E5=92=8C=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/js/script.js b/js/script.js index 8ca1bcd..29e078f 100644 --- a/js/script.js +++ b/js/script.js @@ -2190,7 +2190,23 @@ 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); }); @@ -2425,6 +2441,9 @@ 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; // 字体大小也按比例缩放 @@ -2439,15 +2458,46 @@ lineHeight = lineHeight * scale; } - // 微信中:使用更粗的字体权重以获得更好的可读性 + // 处理字体粗细 - 统一转换为标准格式 let fontWeight = style.fontWeight; - if (isWechat() && (fontWeight === '400' || fontWeight === 'normal')) { - fontWeight = '500'; // 稍微加粗一点 + // 数值转换为关键字 + if (fontWeight === '400' || fontWeight === 'normal') fontWeight = 'normal'; + else if (fontWeight === '700' || fontWeight === 'bold') fontWeight = 'bold'; + // 微信中:使用更粗的字体权重以获得更好的可读性 + if (isWechat() && fontWeight === 'normal') { + fontWeight = '500'; } - // 使用高清字体渲染 - ctx.font = `${fontWeight} ${fontSize}px ${style.fontFamily}`; + // 获取斜体样式 + 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'; // 微信中:开启文字抗锯齿 From 9849d4e3d462692ede87a15bd358a3b962f638ca Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 10:00:21 +0800 Subject: [PATCH 14/19] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E7=AB=AF=E5=AD=97=E4=BD=93=E6=9D=83=E9=87=8D=E7=89=B9?= =?UTF-8?q?=E6=AE=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/script.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/js/script.js b/js/script.js index 29e078f..0a25178 100644 --- a/js/script.js +++ b/js/script.js @@ -2463,10 +2463,6 @@ // 数值转换为关键字 if (fontWeight === '400' || fontWeight === 'normal') fontWeight = 'normal'; else if (fontWeight === '700' || fontWeight === 'bold') fontWeight = 'bold'; - // 微信中:使用更粗的字体权重以获得更好的可读性 - if (isWechat() && fontWeight === 'normal') { - fontWeight = '500'; - } // 获取斜体样式 let fontStyle = style.fontStyle; From d1abd7bc634190d965576e47fa458a05604bb00a Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 21 Apr 2026 10:07:09 +0800 Subject: [PATCH 15/19] =?UTF-8?q?style:=20=E8=B0=83=E6=95=B4=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E4=BD=8D=E7=BD=AE=E5=BE=AE=E8=B0=83=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 81a0974..271c62c 100644 --- a/index.html +++ b/index.html @@ -88,11 +88,11 @@