feat: 绘制海报 未完
This commit is contained in:
294
src/components/Poster/index.tsx
Normal file
294
src/components/Poster/index.tsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { useEffect } 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 ringUrl =
|
||||||
|
"http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png";
|
||||||
|
|
||||||
|
const Poster = (props) => {
|
||||||
|
const carouselUrl =
|
||||||
|
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg";
|
||||||
|
|
||||||
|
const avatarUrl =
|
||||||
|
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/aac792b0-6f81-4192-ae55-04bee417167c.png";
|
||||||
|
useEffect(() => {
|
||||||
|
drawCard();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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.getSystemInfoSync().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, carouselUrl);
|
||||||
|
// roundRect(ctx, 20, 20, width - 40, width - 40, 20, "#fff");
|
||||||
|
await drawCoverImage(
|
||||||
|
ctx,
|
||||||
|
carouselUrl,
|
||||||
|
img,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
width - 20,
|
||||||
|
width - 20,
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
let left = drawTag(ctx, "单打", 18, 18);
|
||||||
|
drawTag(ctx, "NTRP 2.5 - 3.0", 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 = "华巴轮卡 邀你加入";
|
||||||
|
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";
|
||||||
|
const r = drawTextWrap(
|
||||||
|
ctx,
|
||||||
|
"周一晚上浦东新区单打约球",
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width - 32,
|
||||||
|
40
|
||||||
|
);
|
||||||
|
|
||||||
|
top = r.top + 50;
|
||||||
|
|
||||||
|
// 时间
|
||||||
|
ctx.font = "26px sans-serif";
|
||||||
|
ctx.fillStyle = "#00B578";
|
||||||
|
const day = "6月20日 (周五)";
|
||||||
|
ctx.fillText(day, 80, top);
|
||||||
|
textW = ctx.measureText(day).width;
|
||||||
|
left = 80 + 8 + textW;
|
||||||
|
ctx.fillStyle = "#333";
|
||||||
|
ctx.fillText("下午5点 2小时", left, top);
|
||||||
|
|
||||||
|
// 地址
|
||||||
|
ctx.fillStyle = "#666";
|
||||||
|
ctx.font = "26px sans-serif";
|
||||||
|
drawTextWrap(
|
||||||
|
ctx,
|
||||||
|
"因乐驰网球俱乐部 (嘉定江桥万达店)",
|
||||||
|
80,
|
||||||
|
560,
|
||||||
|
480,
|
||||||
|
34
|
||||||
|
);
|
||||||
|
|
||||||
|
// 底部文字
|
||||||
|
ctx.fillStyle = "#333";
|
||||||
|
ctx.font = "24px sans-serif";
|
||||||
|
ctx.fillText("有场 · 网球", 40, 960);
|
||||||
|
|
||||||
|
// 小程序码
|
||||||
|
// 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 Poster;
|
||||||
@@ -25,6 +25,7 @@ import GeneralNavbar from "./GeneralNavbar";
|
|||||||
import RadarChart from './Radar'
|
import RadarChart from './Radar'
|
||||||
import EmptyState from './EmptyState';
|
import EmptyState from './EmptyState';
|
||||||
import NTRPTestEntryCard from './NTRPTestEntryCard'
|
import NTRPTestEntryCard from './NTRPTestEntryCard'
|
||||||
|
import Poster from './Poster'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ActivityTypeSwitch,
|
ActivityTypeSwitch,
|
||||||
@@ -55,4 +56,5 @@ export {
|
|||||||
RadarChart,
|
RadarChart,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
NTRPTestEntryCard,
|
NTRPTestEntryCard,
|
||||||
|
Poster,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
NTRPEvaluatePopup,
|
NTRPEvaluatePopup,
|
||||||
GameManagePopup,
|
GameManagePopup,
|
||||||
Comments,
|
Comments,
|
||||||
|
Poster,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import DetailService, {
|
import DetailService, {
|
||||||
MATCH_STATUS,
|
MATCH_STATUS,
|
||||||
@@ -175,7 +176,7 @@ function Coursel(props) {
|
|||||||
// 分享弹窗
|
// 分享弹窗
|
||||||
const SharePopup = forwardRef(
|
const SharePopup = forwardRef(
|
||||||
({ id, from }: { id: string; from: string }, ref) => {
|
({ id, from }: { id: string; from: string }, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(true);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
show: () => {
|
show: () => {
|
||||||
@@ -216,8 +217,9 @@ const SharePopup = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonPopup
|
<>
|
||||||
title="分享"
|
{/* <CommonPopup
|
||||||
|
title="分享至"
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
@@ -227,24 +229,41 @@ const SharePopup = forwardRef(
|
|||||||
>
|
>
|
||||||
<View className={styles.shareContainer}>
|
<View className={styles.shareContainer}>
|
||||||
<View catchMove className={styles.title}>
|
<View catchMove className={styles.title}>
|
||||||
分享卡片
|
分享至
|
||||||
</View>
|
</View>
|
||||||
<View className={styles.shareItems}>
|
<View className={styles.shareItems}>
|
||||||
<Button
|
<Button
|
||||||
className={classnames(styles.button, styles.share)}
|
className={classnames(styles.button, styles.share)}
|
||||||
openType="share"
|
openType="share"
|
||||||
>
|
>
|
||||||
分享到聊天
|
微信好友
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={classnames(styles.button, styles.save)}
|
className={classnames(styles.button, styles.save)}
|
||||||
onClick={handleSaveToLocal}
|
onClick={handleSaveToLocal}
|
||||||
>
|
>
|
||||||
保存到本地
|
生成分享图
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</CommonPopup> */}
|
||||||
|
<CommonPopup
|
||||||
|
title="分享至"
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
showHeader={false}
|
||||||
|
position="center"
|
||||||
|
hideFooter
|
||||||
|
enableDragToClose={false}
|
||||||
|
style={{ minHeight: "100px" }}
|
||||||
|
>
|
||||||
|
<View className={styles.posterWrap}>
|
||||||
|
<Poster />
|
||||||
|
</View>
|
||||||
</CommonPopup>
|
</CommonPopup>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,3 +22,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.posterWrap {
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user