feat(canvas): 适配微信浏览器并优化导出质量
This commit is contained in:
110
js/script.js
110
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;
|
||||
|
||||
// 微信中:如果原始图片比目标区域大,使用原始尺寸绘制以获得最佳质量
|
||||
// 否则按计算尺寸绘制
|
||||
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,11 +2327,22 @@
|
||||
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;
|
||||
const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85;
|
||||
@@ -2309,22 +2373,41 @@
|
||||
// 在 toBlob 之前再次确认 canvas 尺寸
|
||||
console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height);
|
||||
|
||||
// 尝试使用 toDataURL 获取更高质量的图片
|
||||
// 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损
|
||||
let dataUrl;
|
||||
let 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 {
|
||||
// 先尝试 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');
|
||||
@@ -2333,6 +2416,7 @@
|
||||
});
|
||||
dataUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('导出图片大小:', (blob.size / 1024).toFixed(2), 'KB');
|
||||
const filename = '邀请函_' + Date.now() + '.png';
|
||||
|
||||
Reference in New Issue
Block a user