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');