Files
mini-programs/src/components/Poster/index.tsx

356 lines
9.7 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 { 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);