From 2c739255b73c9920e266f076307d724df55e85d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Tue, 25 Nov 2025 11:29:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=A6=E6=83=85=E9=A1=B5=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E6=B5=B7=E6=8A=A5=E9=87=8D=E6=96=B0=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game_pages/sharePoster/index.tsx | 13 +- src/utils/genPoster.ts | 176 ++++++++++++++++++++++----- 2 files changed, 150 insertions(+), 39 deletions(-) diff --git a/src/game_pages/sharePoster/index.tsx b/src/game_pages/sharePoster/index.tsx index 3f32c63..2c6211c 100644 --- a/src/game_pages/sharePoster/index.tsx +++ b/src/game_pages/sharePoster/index.tsx @@ -4,6 +4,7 @@ import { View, Image, Text, Button } from "@tarojs/components"; import Taro, { useRouter } from "@tarojs/taro"; import classnames from "classnames"; import dayjs from "dayjs"; +import "dayjs/locale/zh-cn"; import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils"; import { withAuth } from "@/components"; import GeneralNavbar from "@/components/GeneralNavbar"; @@ -17,6 +18,8 @@ import { genNTRPRequirementText } from "@/utils/helper"; import { waitForAuthInit } from "@/utils/authInit"; import styles from "./index.module.scss"; +dayjs.locale("zh-cn"); + function SharePoster(props) { const [url, setUrl] = useState(""); const { fetchUserInfo } = useUserActions(); @@ -63,8 +66,9 @@ function SharePoster(props) { playType: play_type, ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, mainCoursal: - image_list[0] || - "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", + image_list[0] && image_list[0].startsWith("http") + ? image_list[0] + : "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", nickname, avatarUrl: avatar_url, title, @@ -84,10 +88,7 @@ function SharePoster(props) { return ( <> - + {url && ( diff --git a/src/utils/genPoster.ts b/src/utils/genPoster.ts index c7b5fb7..a690046 100644 --- a/src/utils/genPoster.ts +++ b/src/utils/genPoster.ts @@ -3,6 +3,8 @@ import Taro from "@tarojs/taro"; // const qrCodeUrl = // "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png"; +const bgUrl = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png"; + const ringUrl = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; @@ -64,6 +66,8 @@ function loadImage(canvas: any, src: string): Promise { }); } +const deg2rad = d => d * Math.PI / 180; + /** 圆角矩形渐变 */ function roundRectGradient( ctx: any, @@ -131,6 +135,57 @@ async function drawCoverImage( 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, @@ -156,9 +211,47 @@ function roundRect( 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 = "22px sans-serif"; + 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"); @@ -212,26 +305,42 @@ export async function generatePosterImage(data: any): Promise { // 背景渐变 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 drawCoverImage( + await drawRotateCoverImage( ctx, canvas, data.mainCoursal, mainImg, - 10, - 10, - width - 20, - width - 20, - 20 + 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, 18, 18); - drawTag(ctx, data.ntrp, left + 4, 18); + let left = drawTag(ctx, data.playType, 20, 20); + drawTag(ctx, data.ntrp, left + 8, 20); let top = width - 10 + 16; - left = 16; + left = 20; // 用户头像 const avatarImg = await loadImage(canvas, data.avatarUrl); @@ -246,7 +355,7 @@ export async function generatePosterImage(data: any): Promise { top += 40; // 用户名 + 邀请 - ctx.fillStyle = "#333"; + ctx.fillStyle = "#000"; ctx.font = "bold 28px sans-serif"; const nickNameText = `${data.nickname} 邀你加入`; ctx.fillText(nickNameText, left, top); @@ -258,16 +367,16 @@ export async function generatePosterImage(data: any): Promise { const ringImg = await loadImage(canvas, ringUrl); ctx.drawImage(ringImg, left - 10, top - 30, 80, 36); - left = 16; + left = 20; top += 60; // 活动标题 - ctx.fillStyle = "#333"; + ctx.fillStyle = "#000"; ctx.font = "bold 34px sans-serif"; let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40); - top = r.top + 40; - left = 16; + top = r.top + 30; + left = 20; const dateImg = await loadImage(canvas, dateIcon); await drawCoverImage( @@ -279,34 +388,34 @@ export async function generatePosterImage(data: any): Promise { top, 40, 40, - 8 + 12 ); - left += 40 + 8; + left += 40 + 16; top += 30; - ctx.font = "26px sans-serif"; + 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 = "#333"; + ctx.fillStyle = "#000"; ctx.fillText(data.time, left, top); - left = 16; + left = 20; top += 24; const mapImg = await loadImage(canvas, mapIcon); - await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 8); + await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 12); - left += 40 + 8; + left += 40 + 16; top += 30; - ctx.fillStyle = "#666"; - ctx.font = "26px sans-serif"; + ctx.fillStyle = "#000"; + ctx.font = "500 26px sans-serif"; r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34); - left = 16; + left = 20; top = r.top + 60; const logoWh = await getImageWh(logoText); @@ -314,22 +423,23 @@ export async function generatePosterImage(data: any): Promise { ctx.drawImage( logoTextImg, left, - top, + // top, + height - logoWh.height - 30 - 30, 400, 400 / (logoWh.width / logoWh.height) ); const qrImg = await loadImage(canvas, data.qrCodeUrl); - roundRectGradient(ctx, width - 12 - 150, top - 50, 140, 140, 20, "#fff", "#fff") - ctx.drawImage(qrImg, width - 12 - 145, top - 45, 130, 130); + // 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 = 16; - top += 400 / (logoWh.width / logoWh.height) + 30; + left = 20; + // top += 400 / (logoWh.width / logoWh.height) + 30; - ctx.fillStyle = "#333"; - ctx.font = "20px sans-serif"; - ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top); + 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({