451 lines
11 KiB
TypeScript
451 lines
11 KiB
TypeScript
import Taro from "@tarojs/taro";
|
||
import { OSS_BASE } from "@/config/api";
|
||
|
||
const bgUrl = `${OSS_BASE}/front/ball/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`;
|
||
const ringUrl = `${OSS_BASE}/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
|
||
const dateIcon = `${OSS_BASE}/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
|
||
const mapIcon = `${OSS_BASE}/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
|
||
const logoText = `${OSS_BASE}/system/youchang_tip_text.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,
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
interface TaroGetImageInfo {
|
||
getImageInfo(option: {
|
||
src: string;
|
||
success?: (res: { width: number; height: number }) => void;
|
||
fail?: (err: unknown) => void;
|
||
}): void;
|
||
}
|
||
|
||
/** 获取图片宽高 */
|
||
function getImageWh(src: string): Promise<{ width: number; height: number }> {
|
||
return new Promise((resolve, reject) => {
|
||
(Taro as TaroGetImageInfo).getImageInfo({
|
||
src,
|
||
success: ({ width, height }) => resolve({ width, height }),
|
||
fail: (e) => reject(e),
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 加载图片 */
|
||
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 dpr = 1;
|
||
// console.log(dpr, 'dpr')
|
||
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: 0.7,
|
||
});
|
||
return tempFilePath;
|
||
}
|