356 lines
9.7 KiB
TypeScript
356 lines
9.7 KiB
TypeScript
import { useEffect, useImperativeHandle, forwardRef } from "react";
|
||
import { Canvas } from "@tarojs/components";
|
||
import Taro from "@tarojs/taro";
|
||
|
||
function getImageWh(src): Promise<{ width: number; height: number }> {
|
||
return new Promise((resolve) => {
|
||
Taro.getImageInfo({
|
||
src,
|
||
success: ({ width, height }) => resolve({ width, height }),
|
||
});
|
||
});
|
||
}
|
||
|
||
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";
|
||
|
||
const Poster = (props, ref) => {
|
||
const { data } = props;
|
||
const {
|
||
playType,
|
||
ntrp,
|
||
mainCoursal,
|
||
nickname,
|
||
avatarUrl,
|
||
title,
|
||
locationName,
|
||
date,
|
||
time,
|
||
} = data;
|
||
useEffect(() => {
|
||
drawCard();
|
||
}, []);
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
generateImage: () =>
|
||
new Promise((resolve, reject) => {
|
||
const query = Taro.createSelectorQuery();
|
||
query
|
||
.select("#cardCanvas")
|
||
.fields({ node: true, size: true })
|
||
.exec((res) => {
|
||
const canvas = res[0].node;
|
||
// ⚠️ 关键:传 canvas,而不是 canvasId
|
||
Taro.canvasToTempFilePath({
|
||
canvas,
|
||
success: (res) => resolve(res.tempFilePath),
|
||
fail: (err) => reject(err),
|
||
});
|
||
});
|
||
}),
|
||
}));
|
||
|
||
const drawCard = async () => {
|
||
const query = Taro.createSelectorQuery();
|
||
query
|
||
.select("#cardCanvas")
|
||
.fields({ node: true, size: true })
|
||
.exec(async (res) => {
|
||
const canvas = res[0].node;
|
||
const ctx = canvas.getContext("2d");
|
||
const dpr = Taro.getWindowInfo().pixelRatio;
|
||
const width = 600; // px
|
||
const height = 1000;
|
||
|
||
canvas.width = width * dpr;
|
||
canvas.height = height * dpr;
|
||
ctx.scale(dpr, dpr);
|
||
|
||
// 背景卡片
|
||
// roundRect(ctx, 0, 0, width, height, 20, "#fff");
|
||
|
||
// 整体背景
|
||
roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC");
|
||
|
||
// 顶部图片
|
||
const img = await loadImage(canvas, mainCoursal);
|
||
// roundRect(ctx, 20, 20, width - 40, width - 40, 20, "#fff");
|
||
await drawCoverImage(
|
||
ctx,
|
||
mainCoursal,
|
||
img,
|
||
10,
|
||
10,
|
||
width - 20,
|
||
width - 20,
|
||
20
|
||
);
|
||
|
||
// 标签
|
||
let left = drawTag(ctx, playType, 18, 18);
|
||
drawTag(ctx, ntrp, left + 4, 18);
|
||
|
||
let top = width - 10;
|
||
left = 16;
|
||
|
||
top += 16;
|
||
|
||
// 用户头像(圆形)
|
||
const avatar = await loadImage(canvas, avatarUrl);
|
||
ctx.save();
|
||
ctx.beginPath();
|
||
ctx.arc(left + 30, top + 30, 30, 0, Math.PI * 2);
|
||
ctx.clip();
|
||
ctx.drawImage(avatar, left, top, 60, 60);
|
||
ctx.restore();
|
||
|
||
// 头像右边 6
|
||
left += 66;
|
||
|
||
top += 40;
|
||
|
||
// 用户名 + 邀请
|
||
ctx.fillStyle = "#333";
|
||
ctx.font = "bold 28px sans-serif";
|
||
// ctx.fillText("华巴轮卡 邀你加入球局", 100, 370);
|
||
const nickNameText = `${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, title, left, top, width - 32, 40);
|
||
|
||
top = r.top + 40;
|
||
left = 16;
|
||
const dateImg = await loadImage(canvas, dateIcon);
|
||
await drawCoverImage(ctx, dateIcon, dateImg, left, top, 40, 40, 8);
|
||
|
||
left += 40 + 8;
|
||
|
||
top += 30;
|
||
|
||
// 时间
|
||
ctx.font = "26px sans-serif";
|
||
ctx.fillStyle = "#00B578";
|
||
ctx.fillText(date, left, top);
|
||
textW = ctx.measureText(date).width;
|
||
left += 8 + textW;
|
||
ctx.fillStyle = "#333";
|
||
ctx.fillText(time, left, top);
|
||
|
||
left = 16;
|
||
top += 24;
|
||
const mapImg = await loadImage(canvas, mapIcon);
|
||
await drawCoverImage(ctx, dateIcon, mapImg, left, top, 40, 40, 8);
|
||
|
||
left += 40 + 8;
|
||
top += 30;
|
||
|
||
// 地址
|
||
ctx.fillStyle = "#666";
|
||
ctx.font = "26px sans-serif";
|
||
r = drawTextWrap(ctx, locationName, left, top, width - 32 - left, 34);
|
||
|
||
left = 16;
|
||
top = r.top + 60;
|
||
|
||
const logoWh = await getImageWh(logoText);
|
||
console.log(logoWh);
|
||
const logoTextImg = await loadImage(canvas, logoText);
|
||
ctx.drawImage(
|
||
logoTextImg,
|
||
left,
|
||
top,
|
||
400,
|
||
// 56
|
||
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 qrcode = await loadImage(canvas, "小程序码路径");
|
||
// ctx.drawImage(qrcode, 480, 880, 100, 100);
|
||
|
||
// 导出图片
|
||
// Taro.canvasToTempFilePath({
|
||
// canvas,
|
||
// success: (res) => {
|
||
// console.log("导出路径", res.tempFilePath);
|
||
// },
|
||
// fail: (err) => console.error(err),
|
||
// });
|
||
});
|
||
};
|
||
|
||
const roundRectGradient = (ctx, x, y, w, h, r, color1, color2) => {
|
||
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();
|
||
};
|
||
|
||
const drawCoverImage = async (ctx, src, img, x, y, w, h, r = 0) => {
|
||
const { width, height } = await getImageWh(src);
|
||
const imgW = width;
|
||
const imgH = height;
|
||
|
||
// 计算缩放比例,取较大值,保证完全覆盖
|
||
const scale = Math.max(w / imgW, h / imgH);
|
||
|
||
// 缩放后的宽高
|
||
const newW = imgW * scale;
|
||
const newH = imgH * 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();
|
||
}
|
||
|
||
// 绘制图片 (cover 效果)
|
||
ctx.drawImage(img, offsetX, offsetY, newW, newH);
|
||
|
||
ctx.restore();
|
||
};
|
||
|
||
/** 加载图片 */
|
||
const loadImage = (canvas, src) => {
|
||
return new Promise((resolve) => {
|
||
const img = canvas.createImage();
|
||
img.onload = () => resolve(img);
|
||
img.src = src;
|
||
});
|
||
};
|
||
|
||
/** 圆角矩形 */
|
||
const roundRect = (ctx, x, y, w, h, r, fillStyle) => {
|
||
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();
|
||
};
|
||
|
||
/** 绘制标签 */
|
||
const drawTag = (ctx, text, x, y) => {
|
||
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;
|
||
};
|
||
|
||
/** 文本换行 */
|
||
const drawTextWrap = (ctx, text, x, y, maxWidth, lineHeight) => {
|
||
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),
|
||
top: y + (lines.length - 1) * lineHeight,
|
||
};
|
||
};
|
||
|
||
return (
|
||
<Canvas
|
||
type="2d"
|
||
id="cardCanvas"
|
||
style={{
|
||
width: "600rpx",
|
||
height: "1000rpx",
|
||
// position: "absolute",
|
||
// left: "-9999px",
|
||
}}
|
||
/>
|
||
);
|
||
};
|
||
|
||
export default forwardRef(Poster);
|