Files
mini-programs/src/utils/genPoster.ts
2025-12-04 11:25:22 +08:00

448 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Taro from "@tarojs/taro";
import { OSS_BASE_URL } from "@/config/api";
// const qrCodeUrl = `${OSS_BASE_URL}/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png`;
const bgUrl = `${OSS_BASE_URL}/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`;
const ringUrl = `${OSS_BASE_URL}/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
const dateIcon = `${OSS_BASE_URL}/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
const mapIcon = `${OSS_BASE_URL}/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
// const logo = `${OSS_BASE_URL}/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg`
const logoText = `${OSS_BASE_URL}/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png`;
export function base64ToTempFilePath(base64Data: string): Promise<string> {
return new Promise((resolve, reject) => {
const fsm = Taro.getFileSystemManager();
const filePath = `${Taro.env.USER_DATA_PATH}/temp_qrcode_${Date.now()}.png`;
// 去掉 data:image/png;base64, 前缀(如果有)
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
// 将base64转换成ArrayBuffer
const arrayBuffer = Taro.base64ToArrayBuffer(base64);
fsm.writeFile({
filePath,
data: arrayBuffer,
encoding: 'binary', // 这里使用'binary'
success: () => {
fsm.access({
path: filePath,
success: () => resolve(filePath),
fail: (e) => reject(e),
});
},
fail: reject,
});
});
}
/** 获取图片宽高 */
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;
});
}
const deg2rad = d => d * Math.PI / 180;
/** 圆角矩形渐变 */
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();
}
/** 绘制 cover 图片(支持圆角 + 旋转) */
async function drawRotateCoverImage(
ctx: any,
canvas: any,
src: string,
img: any,
x: number,
y: number,
w: number,
h: number,
r = 0,
rotate = 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();
// 以 cover 区域中心为旋转点
const cx = x + w / 2;
const cy = y + h / 2;
ctx.translate(cx, cy);
ctx.rotate(rotate);
ctx.translate(-cx, -cy);
// ------- clipping注意要在旋转变换之后做 path-------
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();
}
// 绘制 cover
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 roundRotateRect(
ctx: any,
x: number,
y: number,
w: number,
h: number,
r: number,
fillStyle: string,
rotate: number = 0 // 弧度
) {
ctx.save();
// 以圆角矩形中心作为旋转点
const cx = x + w / 2;
const cy = y + h / 2;
ctx.translate(cx, cy);
ctx.rotate(rotate);
ctx.translate(-cx, -cy);
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();
ctx.restore();
}
/** 绘制标签 */
function drawTag(ctx: any, text: string, x: number, y: number) {
ctx.font = "600 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.getWindowInfo().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 bgImg = await loadImage(canvas, bgUrl);
ctx.drawImage(bgImg, 0, 0, width, height);
roundRotateRect(ctx, 70, 100, width - 140, width - 140, 20, '#fff', deg2rad(-6));
// 顶部图片
const mainImg = await loadImage(canvas, data.mainCoursal);
await drawRotateCoverImage(
ctx,
canvas,
data.mainCoursal,
mainImg,
75,
105,
width - 150,
width - 150,
20,
deg2rad(-6),
);
// await drawCoverImage(
// ctx,
// canvas,
// data.mainCoursal,
// mainImg,
// 10,
// 10,
// width - 20,
// width - 20,
// 20
// );
// 标签
let left = drawTag(ctx, data.playType, 20, 20);
drawTag(ctx, data.ntrp, left + 8, 20);
let top = width - 10 + 16;
left = 20;
// 用户头像
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 = "#000";
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 = 20;
top += 60;
// 活动标题
ctx.fillStyle = "#000";
ctx.font = "bold 34px sans-serif";
let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40);
top = r.top + 30;
left = 20;
const dateImg = await loadImage(canvas, dateIcon);
await drawCoverImage(
ctx,
canvas,
dateIcon,
dateImg,
left,
top,
40,
40,
12
);
left += 40 + 16;
top += 30;
ctx.font = "500 26px sans-serif";
ctx.fillStyle = "#00B578";
ctx.fillText(data.date, left, top);
textW = ctx.measureText(data.date).width;
left += 8 + textW;
ctx.fillStyle = "#000";
ctx.fillText(data.time, left, top);
left = 20;
top += 24;
const mapImg = await loadImage(canvas, mapIcon);
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 12);
left += 40 + 16;
top += 30;
ctx.fillStyle = "#000";
ctx.font = "500 26px sans-serif";
r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34);
left = 20;
top = r.top + 60;
const logoWh = await getImageWh(logoText);
const logoTextImg = await loadImage(canvas, logoText);
ctx.drawImage(
logoTextImg,
left,
// top,
height - logoWh.height - 30 - 30,
400,
400 / (logoWh.width / logoWh.height)
);
const qrImg = await loadImage(canvas, data.qrCodeUrl);
// roundRectGradient(ctx, width - 12 - 150, height - 22 - 140, 140, 140, 20, "#fff", "#fff")
ctx.drawImage(qrImg, width - 22 - 100, height - 22 - 100 - 2, 100, 100);
left = 20;
// top += 400 / (logoWh.width / logoWh.height) + 30;
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
ctx.font = "400 20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, height - 30/* top */);
// 导出图片
const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
});
return tempFilePath;
}