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 { 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 { return new Promise((resolve, reject) => { let timer: any; const img = canvas.createImage(); img.crossOrigin = "anonymous" img.onload = () => { clearTimeout(timer); resolve(img); }; img.onerror = () => { clearTimeout(timer); console.log('img error', src) } timer = setTimeout(() => { reject(new Error(`Image load timeout: ${src}`)); }, 8000); 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); console.log('width', width, 'height', height) 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); console.log('drawImage', 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 { console.log("start !!!!"); // const dpr = Taro.getWindowInfo().pixelRatio; const dpr = 1; // console.log(dpr, 'dpr') const width = 600; const height = 1000; console.log('width', width, 'height', height) const canvas = Taro.createOffscreenCanvas({ type: "2d", width: width * dpr, height: height * dpr }); const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); console.log('ctx', ctx) // 背景渐变 roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC"); console.log('bgUrl', bgUrl) const bgImg = await loadImage(canvas, bgUrl); ctx.drawImage(bgImg, 0, 0, width, height); console.log('bgUrlend', ) roundRotateRect(ctx, 70, 100, width - 140, width - 140, 20, '#fff', deg2rad(-6)); // 顶部图片 const mainImg = await loadImage(canvas, data.mainCoursal); console.log('mainCoursal', 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); console.log('dateIcon', 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 */); console.log('canvas', canvas) // 导出图片 const { tempFilePath } = await Taro.canvasToTempFilePath({ canvas, fileType: 'png', quality: 0.7, }); console.log('tempFilePath', tempFilePath) return tempFilePath; }