feat(canvas): 适配微信浏览器并优化导出质量
This commit is contained in:
146
js/script.js
146
js/script.js
@@ -2006,21 +2006,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成画布(高清无损,固定 1080×1920 分辨率)
|
// 检测是否在微信内置浏览器
|
||||||
|
function isWechat() {
|
||||||
|
return /MicroMessenger/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成画布(高清无损,根据设备像素比自适应分辨率)
|
||||||
async function generateCanvas() {
|
async function generateCanvas() {
|
||||||
const canvas = exportCanvas;
|
const canvas = exportCanvas;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const container = imageContainer;
|
const container = imageContainer;
|
||||||
|
|
||||||
// 固定高清分辨率 1080×1920
|
// 根据设备像素比决定输出分辨率,移动端至少 2x
|
||||||
const TARGET_WIDTH = 1080;
|
// 微信等浏览器 canvas 最大尺寸通常为 4096,所以上限设为 2160×3840
|
||||||
const TARGET_HEIGHT = 1920;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const minScale = Math.max(dpr, 2); // 至少 2x
|
||||||
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT);
|
|
||||||
|
|
||||||
// 显示尺寸(用于计算缩放比例)
|
|
||||||
const displayWidth = container.clientWidth;
|
const displayWidth = container.clientWidth;
|
||||||
const displayHeight = container.clientHeight;
|
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);
|
console.log('显示容器尺寸:', displayWidth, 'x', displayHeight);
|
||||||
|
|
||||||
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
|
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
|
||||||
@@ -2122,8 +2147,36 @@
|
|||||||
const drawY = (containerHeight - scaledHeight) / 2;
|
const drawY = (containerHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
ctx.globalAlpha = bgImageOpacity;
|
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;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2274,10 +2327,21 @@
|
|||||||
const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放
|
const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放
|
||||||
const lineHeight = parseFloat(style.lineHeight);
|
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.fillStyle = style.color;
|
||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
|
// 微信中:开启文字抗锯齿
|
||||||
|
if (isWechat()) {
|
||||||
|
ctx.textRendering = 'optimizeLegibility';
|
||||||
|
}
|
||||||
|
|
||||||
// 计算文本垂直偏移量,使其与 DOM 渲染一致
|
// 计算文本垂直偏移量,使其与 DOM 渲染一致
|
||||||
const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY;
|
const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY;
|
||||||
@@ -2309,29 +2373,49 @@
|
|||||||
// 在 toBlob 之前再次确认 canvas 尺寸
|
// 在 toBlob 之前再次确认 canvas 尺寸
|
||||||
console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height);
|
console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height);
|
||||||
|
|
||||||
// 尝试使用 toDataURL 获取更高质量的图片
|
// 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损
|
||||||
let dataUrl;
|
let dataUrl;
|
||||||
let blob;
|
let blob;
|
||||||
|
|
||||||
try {
|
// 微信中优先使用 toBlob,避免 toDataURL 的内存问题
|
||||||
// 先尝试 toDataURL
|
if (isWechat()) {
|
||||||
dataUrl = canvas.toDataURL('image/png');
|
try {
|
||||||
console.log('toDataURL 成功,dataUrl 长度:', dataUrl.length);
|
blob = await new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((b) => {
|
||||||
// 将 dataUrl 转换为 blob
|
if (b) {
|
||||||
const response = await fetch(dataUrl);
|
console.log('微信 toBlob 成功,blob 尺寸:', b.size, 'bytes');
|
||||||
blob = await response.blob();
|
resolve(b);
|
||||||
console.log('dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes');
|
} else {
|
||||||
} catch (err) {
|
reject(new Error('toBlob 返回 null'));
|
||||||
console.log('toDataURL 失败,使用 toBlob:', err);
|
}
|
||||||
// 如果 toDataURL 失败,使用 toBlob
|
}, 'image/png', 1.0); // 1.0 = 最高质量无损 PNG
|
||||||
blob = await new Promise(resolve => {
|
});
|
||||||
canvas.toBlob((b) => {
|
dataUrl = URL.createObjectURL(blob);
|
||||||
console.log('toBlob 回调 - blob 尺寸:', b.size, 'bytes');
|
} catch (err) {
|
||||||
resolve(b);
|
console.log('微信 toBlob 失败,回退到 toDataURL:', err);
|
||||||
}, 'image/png', 1.0);
|
dataUrl = canvas.toDataURL('image/png');
|
||||||
});
|
const response = await fetch(dataUrl);
|
||||||
dataUrl = URL.createObjectURL(blob);
|
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');
|
console.log('导出图片大小:', (blob.size / 1024).toFixed(2), 'KB');
|
||||||
|
|||||||
Reference in New Issue
Block a user