feat: 生成海报

This commit is contained in:
2025-10-03 09:19:05 +08:00
parent 40a043d2a0
commit 5fec10b342
18 changed files with 1032 additions and 200 deletions

312
src/utils/genPoster.ts Normal file
View File

@@ -0,0 +1,312 @@
import Taro from "@tarojs/taro";
const qrCodeUrl =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png";
const ringUrl =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png";
const dateIcon =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg";
const mapIcon =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg";
// const logo = "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg"
const logoText =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png";
/** 获取图片宽高 */
function getImageWh(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
Taro.getImageInfo({
src,
success: ({ width, height }) => resolve({ width, height }),
});
});
}
/** 加载图片 */
function loadImage(canvas: any, src: string): Promise<any> {
return new Promise((resolve) => {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.src = src;
});
}
/** 圆角矩形渐变 */
function roundRectGradient(
ctx: any,
x: number,
y: number,
w: number,
h: number,
r: number,
color1: string,
color2: string
) {
const gradient = ctx.createLinearGradient(x, y, x, y + h);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
}
/** 绘制 cover 图片(支持圆角) */
async function drawCoverImage(
ctx: any,
canvas: any,
src: string,
img: any,
x: number,
y: number,
w: number,
h: number,
r = 0
) {
const { width, height } = await getImageWh(src);
const scale = Math.max(w / width, h / height);
const newW = width * scale;
const newH = height * scale;
const offsetX = x + (w - newW) / 2;
const offsetY = y + (h - newH) / 2;
ctx.save();
if (r > 0) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.clip();
}
ctx.drawImage(img, offsetX, offsetY, newW, newH);
ctx.restore();
}
/** 圆角矩形 */
function roundRect(
ctx: any,
x: number,
y: number,
w: number,
h: number,
r: number,
fillStyle: string
) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
}
/** 绘制标签 */
function drawTag(ctx: any, text: string, x: number, y: number) {
ctx.font = "22px sans-serif";
const padding = 12;
const textWidth = ctx.measureText(text).width;
roundRect(ctx, x, y, textWidth + padding * 2, 40, 20, "#fff");
ctx.fillStyle = "#333";
ctx.fillText(text, x + padding, y + 28);
return x + textWidth + padding * 2;
}
/** 文本换行 */
function drawTextWrap(
ctx: any,
text: string,
x: number,
y: number,
maxWidth: number,
lineHeight: number
) {
let line = "";
const lines = [];
for (let char of text) {
const testLine = line + char;
if (ctx.measureText(testLine).width > maxWidth) {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
if (line) lines.push(line);
lines.forEach((l, i) => {
ctx.fillText(l, x, y + i * lineHeight);
});
const lastLineText = lines.at(-1);
return {
left: x + ctx.measureText(lastLineText).width,
top: y + (lines.length - 1) * lineHeight,
};
}
/** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!");
const dpr = Taro.getSystemInfoSync().pixelRatio;
const width = 600;
const height = 1000;
const canvas = Taro.createOffscreenCanvas({ type: "2d", width: width * dpr, height: height * dpr });
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
// 背景渐变
roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC");
// 顶部图片
const mainImg = await loadImage(canvas, data.mainCoursal);
console.log(222);
await drawCoverImage(
ctx,
canvas,
data.mainCoursal,
mainImg,
10,
10,
width - 20,
width - 20,
20
);
// 标签
let left = drawTag(ctx, data.playType, 18, 18);
drawTag(ctx, data.ntrp, left + 4, 18);
let top = width - 10 + 16;
left = 16;
// 用户头像
const avatarImg = await loadImage(canvas, data.avatarUrl);
ctx.save();
ctx.beginPath();
ctx.arc(left + 30, top + 30, 30, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(avatarImg, left, top, 60, 60);
ctx.restore();
left += 66;
top += 40;
// 用户名 + 邀请
ctx.fillStyle = "#333";
ctx.font = "bold 28px sans-serif";
const nickNameText = `${data.nickname} 邀你加入`;
ctx.fillText(nickNameText, left, top);
let textW = ctx.measureText(nickNameText).width;
left += textW;
ctx.fillStyle = "#00B578";
ctx.fillText("球局", left, top);
const ringImg = await loadImage(canvas, ringUrl);
ctx.drawImage(ringImg, left - 10, top - 30, 80, 36);
left = 16;
top += 60;
// 活动标题
ctx.fillStyle = "#333";
ctx.font = "bold 34px sans-serif";
let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40);
top = r.top + 40;
left = 16;
const dateImg = await loadImage(canvas, dateIcon);
await drawCoverImage(
ctx,
canvas,
dateIcon,
dateImg,
left,
top,
40,
40,
8
);
left += 40 + 8;
top += 30;
ctx.font = "26px sans-serif";
ctx.fillStyle = "#00B578";
ctx.fillText(data.date, left, top);
textW = ctx.measureText(data.date).width;
left += 8 + textW;
ctx.fillStyle = "#333";
ctx.fillText(data.time, left, top);
left = 16;
top += 24;
const mapImg = await loadImage(canvas, mapIcon);
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 8);
left += 40 + 8;
top += 30;
ctx.fillStyle = "#666";
ctx.font = "26px sans-serif";
r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34);
left = 16;
top = r.top + 60;
const logoWh = await getImageWh(logoText);
const logoTextImg = await loadImage(canvas, logoText);
ctx.drawImage(
logoTextImg,
left,
top,
400,
400 / (logoWh.width / logoWh.height)
);
const qrImg = await loadImage(canvas, qrCodeUrl);
ctx.drawImage(qrImg, width - 12 - 150, top - 50, 160, 160);
left = 16;
top += 400 / (logoWh.width / logoWh.height) + 30;
ctx.fillStyle = "#333";
ctx.font = "20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top);
// 导出图片
const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
});
return tempFilePath;
}

View File

@@ -7,3 +7,5 @@ export * from "./tokenManager";
export * from "./order.pay";
export * from './orderActions';
export * from './routeUtil';
export * from './share'
export * from './genPoster'