fix: card content font
This commit is contained in:
@@ -228,7 +228,8 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
canvas-id="cardCanvas"
|
type="2d"
|
||||||
|
id="cardCanvas"
|
||||||
class="hidden-canvas"
|
class="hidden-canvas"
|
||||||
style="width: 540px; height: 960px"
|
style="width: 540px; height: 960px"
|
||||||
/>
|
/>
|
||||||
@@ -275,12 +276,17 @@ const fontList = [
|
|||||||
{
|
{
|
||||||
name: "手写",
|
name: "手写",
|
||||||
family: "ShouXie",
|
family: "ShouXie",
|
||||||
url: "https://file.lihailezzc.com/resource/font/shouxie.ttf", // 示例地址
|
url: "https://file.lihailezzc.com/ZhiMangXing-Regular.ttf", // 示例地址
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "可爱",
|
name: "可爱",
|
||||||
family: "KeAi",
|
family: "KeAi",
|
||||||
url: "https://file.lihailezzc.com/resource/font/keai.ttf", // 示例地址
|
url: "https://file.lihailezzc.com/ZCOOLKuaiLe-Regular.ttf", // 示例地址
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "草书",
|
||||||
|
family: "LiuJianMaoCao",
|
||||||
|
url: "https://file.lihailezzc.com/LiuJianMaoCao-Regular.ttf", // 示例地址
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const selectedFont = ref(fontList[0]);
|
const selectedFont = ref(fontList[0]);
|
||||||
@@ -289,8 +295,10 @@ const changeFont = (font) => {
|
|||||||
if (font.url) {
|
if (font.url) {
|
||||||
uni.showLoading({ title: "加载字体中" });
|
uni.showLoading({ title: "加载字体中" });
|
||||||
uni.loadFontFace({
|
uni.loadFontFace({
|
||||||
|
global: true,
|
||||||
family: font.family,
|
family: font.family,
|
||||||
source: `url("${font.url}")`,
|
source: `url("${font.url}")`,
|
||||||
|
scopes: ["webview", "native"],
|
||||||
success: () => {
|
success: () => {
|
||||||
selectedFont.value = font;
|
selectedFont.value = font;
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
@@ -496,35 +504,68 @@ const showMore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveByCanvas = async (save = true) => {
|
const saveByCanvas = async (save = true) => {
|
||||||
const ctx = uni.createCanvasContext("cardCanvas");
|
return new Promise((resolve, reject) => {
|
||||||
|
const query = uni.createSelectorQuery();
|
||||||
|
query
|
||||||
|
.select("#cardCanvas")
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec(async (res) => {
|
||||||
|
if (!res[0] || !res[0].node) {
|
||||||
|
reject("Canvas not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = res[0].node;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
// 初始化画布尺寸
|
||||||
|
const dpr = uni.getSystemInfoSync().pixelRatio;
|
||||||
|
// 保持 540x960 的逻辑尺寸,为了清晰度可以考虑 * dpr,
|
||||||
|
// 但为了保持和原来一致的输出尺寸,这里先固定物理尺寸
|
||||||
|
// 如果要高清,可以 set width = 540 * dpr,然后 scale(dpr, dpr)
|
||||||
|
// 这里为了简单兼容原逻辑,我们让物理尺寸等于逻辑尺寸
|
||||||
|
canvas.width = 540;
|
||||||
|
canvas.height = 960;
|
||||||
|
|
||||||
// 画布尺寸(rpx 转 px)
|
// 画布尺寸(rpx 转 px)
|
||||||
const W = 540;
|
const W = 540;
|
||||||
const H = 960;
|
const H = 960;
|
||||||
|
|
||||||
|
// 辅助函数:加载图片为 Image 对象
|
||||||
|
const loadCanvasImage = (url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = canvas.createImage();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = (e) => reject(e);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
// 1️⃣ 画背景
|
// 1️⃣ 画背景
|
||||||
// ⭐ 先加载背景图
|
// ⭐ 先加载背景图
|
||||||
const [bgPath, avatarPath] = await Promise.all([
|
const [bgImg, avatarImg] = await Promise.all([
|
||||||
loadImage(currentTemplate?.value?.imageUrl),
|
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
||||||
loadImage(userAvatar.value),
|
loadCanvasImage(userAvatar.value),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ctx.drawImage(bgPath, 0, 0, W, H);
|
ctx.drawImage(bgImg, 0, 0, W, H);
|
||||||
|
|
||||||
// 2️⃣ 半透明遮罩(和你 UI 一致)
|
// 2️⃣ 半透明遮罩(和你 UI 一致)
|
||||||
ctx.setFillStyle("rgba(0,0,0,0.08)");
|
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
// 3️⃣ 标题
|
// 3️⃣ 标题
|
||||||
ctx.setFillStyle("#ffffff");
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.setFontSize(42);
|
ctx.font = "42px sans-serif"; // 默认字体
|
||||||
ctx.setTextAlign("center");
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
|
||||||
ctx.fillText("新春快乐", W / 2, 120);
|
ctx.fillText("新春快乐", W / 2, 120);
|
||||||
|
|
||||||
ctx.setFontSize(22);
|
ctx.font = "22px sans-serif";
|
||||||
ctx.setGlobalAlpha(0.9);
|
ctx.globalAlpha = 0.9;
|
||||||
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
||||||
ctx.setGlobalAlpha(1);
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
// 4️⃣ 祝福语气泡
|
// 4️⃣ 祝福语气泡
|
||||||
drawBubbleText(ctx, {
|
drawBubbleText(ctx, {
|
||||||
@@ -540,31 +581,36 @@ const saveByCanvas = async (save = true) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
drawUserBubble(ctx, {
|
drawUserBubble(ctx, {
|
||||||
x: 40 + userOffsetX.value,
|
x: 160 + userOffsetX.value,
|
||||||
y: H - 120,
|
y: H - 120,
|
||||||
avatarPath: avatarPath,
|
avatarImg: avatarImg, // 传入 Image 对象
|
||||||
username: signatureName.value,
|
username: signatureName.value,
|
||||||
desc: "送上祝福",
|
desc: "送上祝福",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6️⃣ 输出
|
// 6️⃣ 输出
|
||||||
const tempPath = await new Promise((resolve, reject) => {
|
|
||||||
ctx.draw(false, () => {
|
|
||||||
uni.canvasToTempFilePath({
|
uni.canvasToTempFilePath({
|
||||||
canvasId: "cardCanvas",
|
canvas: canvas, // Canvas 2D 必须传 canvas 实例
|
||||||
|
width: W,
|
||||||
|
height: H,
|
||||||
|
destWidth: W,
|
||||||
|
destHeight: H,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (save) saveImage(res.tempFilePath);
|
if (save) saveImage(res.tempFilePath);
|
||||||
resolve(res.tempFilePath);
|
resolve(res.tempFilePath);
|
||||||
},
|
},
|
||||||
fail: (err) => reject(err),
|
fail: (err) => reject(err),
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Canvas draw error:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return tempPath;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadImage = (url) => {
|
const loadImage = (url) => {
|
||||||
|
// 此函数保留给其他可能用到的地方,但 saveByCanvas 内部使用 loadCanvasImage
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.getImageInfo({
|
uni.getImageInfo({
|
||||||
src: url,
|
src: url,
|
||||||
@@ -607,12 +653,11 @@ function drawBubbleText(ctx, options) {
|
|||||||
fontSize = 32,
|
fontSize = 32,
|
||||||
fontFamily = "PingFang SC",
|
fontFamily = "PingFang SC",
|
||||||
} = options;
|
} = options;
|
||||||
|
console.log(111111, fontFamily);
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
ctx.setFontSize(fontSize);
|
ctx.fillStyle = textColor;
|
||||||
ctx.setFillStyle(textColor);
|
ctx.font = `${fontSize}px '${fontFamily}'`;
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
||||||
ctx.textAlign = "left";
|
ctx.textAlign = "left";
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
|
|
||||||
@@ -654,7 +699,7 @@ function drawBubbleText(ctx, options) {
|
|||||||
// )
|
// )
|
||||||
|
|
||||||
// 4️⃣ 绘制文字
|
// 4️⃣ 绘制文字
|
||||||
ctx.setFillStyle(textColor);
|
ctx.fillStyle = textColor;
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
ctx.fillText(line, x + padding, y + padding + index * lineHeight);
|
ctx.fillText(line, x + padding, y + padding + index * lineHeight);
|
||||||
@@ -665,7 +710,7 @@ function drawUserBubble(ctx, options) {
|
|||||||
const {
|
const {
|
||||||
x = 40, // 气泡起点 x
|
x = 40, // 气泡起点 x
|
||||||
y = 860, // 气泡起点 y
|
y = 860, // 气泡起点 y
|
||||||
avatarPath, // 头像本地路径
|
avatarImg, // CanvasImage 对象
|
||||||
username = "zzc",
|
username = "zzc",
|
||||||
desc = "送上祝福",
|
desc = "送上祝福",
|
||||||
avatarSize = 64, // 头像直径
|
avatarSize = 64, // 头像直径
|
||||||
@@ -678,11 +723,11 @@ function drawUserBubble(ctx, options) {
|
|||||||
|
|
||||||
// 设置字体
|
// 设置字体
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
ctx.font = `${fontSizeName}px PingFang SC`;
|
ctx.font = `${fontSizeName}px 'PingFang SC'`;
|
||||||
|
|
||||||
// 测量文字宽度
|
// 测量文字宽度
|
||||||
const nameWidth = ctx.measureText(username).width;
|
const nameWidth = ctx.measureText(username).width;
|
||||||
ctx.font = `${fontSizeDesc}px PingFang SC`;
|
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
|
||||||
const descWidth = ctx.measureText(desc).width;
|
const descWidth = ctx.measureText(desc).width;
|
||||||
|
|
||||||
// 计算气泡宽度和高度
|
// 计算气泡宽度和高度
|
||||||
@@ -716,19 +761,21 @@ function drawUserBubble(ctx, options) {
|
|||||||
Math.PI * 2,
|
Math.PI * 2,
|
||||||
);
|
);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize);
|
if (avatarImg) {
|
||||||
|
ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
|
||||||
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// 3️⃣ 绘制文字
|
// 3️⃣ 绘制文字
|
||||||
const textX = avatarX + avatarSize + padding;
|
const textX = avatarX + avatarSize + padding;
|
||||||
const textY = y + padding;
|
const textY = y + padding;
|
||||||
ctx.setFillStyle(textColor);
|
ctx.fillStyle = textColor;
|
||||||
ctx.font = `${fontSizeName}px PingFang SC`;
|
ctx.font = `${fontSizeName}px 'PingFang SC'`;
|
||||||
ctx.fillText(username, textX, textY);
|
ctx.fillText(username, textX, textY);
|
||||||
ctx.font = `${fontSizeDesc}px PingFang SC`;
|
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
|
||||||
ctx.setGlobalAlpha(0.6);
|
ctx.globalAlpha = 0.6;
|
||||||
ctx.fillText(desc, textX, textY + fontSizeName + 4);
|
ctx.fillText(desc, textX, textY + fontSizeName + 4);
|
||||||
ctx.setGlobalAlpha(1);
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRoundRect(ctx, x, y, w, h, r, color) {
|
function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||||
|
|||||||
Reference in New Issue
Block a user