Merge pull request '处理分享canvas字体及加粗' (#3) from feature/juguohong/20250816 into master
Reviewed-on: tennis/mini-programs#3
This commit is contained in:
@@ -34,6 +34,9 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径
|
const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径
|
||||||
const [isDrawing, setIsDrawing] = useState(false) // 防止重复绘制
|
const [isDrawing, setIsDrawing] = useState(false) // 防止重复绘制
|
||||||
|
const [canvasNode, setCanvasNode] = useState<any>(null) // 2D Canvas 节点
|
||||||
|
const [ctx2d, setCtx2d] = useState<any>(null) // 2D 上下文
|
||||||
|
const [is2dCtx, setIs2dCtx] = useState(false) // 是否为 2D 上下文
|
||||||
|
|
||||||
// 设计稿尺寸
|
// 设计稿尺寸
|
||||||
const designWidth = 500
|
const designWidth = 500
|
||||||
@@ -54,87 +57,28 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const canvasWidth = designWidth * scale
|
const canvasWidth = designWidth * scale
|
||||||
const canvasHeight = designHeight * scale
|
const canvasHeight = designHeight * scale
|
||||||
|
|
||||||
// 绘制加粗文字(单行)
|
// 2D Canvas 字体设置
|
||||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily?: string) => {
|
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
|
||||||
// 设置字体样式
|
const fam = family || 'Noto Sans SC'
|
||||||
if (fontFamily) {
|
const wt = weight || '500'
|
||||||
try {
|
ctx.font = `${wt} ${fontSize}px "${fam}"`
|
||||||
// 尝试使用setFont方法(如果支持)
|
}
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
// 绘制加粗文字(单行)
|
||||||
ctx.setTextAlign('left')
|
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily: string = 'Noto Sans SC', fontWeight: string = '500') => {
|
||||||
ctx.setTextBaseline('top')
|
// 设置字体样式
|
||||||
|
setFont2D(ctx, fontSize, fontFamily, fontWeight)
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.textBaseline = 'top'
|
||||||
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
// 绘制加粗效果:多次绘制并偏移
|
||||||
ctx.fillText(text, x, y)
|
ctx.fillText(text, x, y)
|
||||||
ctx.fillText(text, x + 1, y)
|
// if (isBold) {
|
||||||
ctx.fillText(text, x, y + 1)
|
// ctx.fillText(text, x + 1, y)
|
||||||
ctx.fillText(text, x + 1, y + 1)
|
// ctx.fillText(text, x, y + 1)
|
||||||
}
|
// ctx.fillText(text, x + 1, y + 1)
|
||||||
|
// }
|
||||||
// 绘制文字(支持自动换行)- 微信小程序版本
|
|
||||||
const drawText = (ctx: any, text: string, x: number, y: number, maxWidth: number, fontSize: number, color: string, bold: boolean = false, fontFamily?: string) => {
|
|
||||||
// 设置字体样式
|
|
||||||
if (fontFamily) {
|
|
||||||
try {
|
|
||||||
// 尝试使用setFont方法(如果支持)
|
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
|
||||||
ctx.setTextAlign('left')
|
|
||||||
ctx.setTextBaseline('top')
|
|
||||||
|
|
||||||
const words = text.split('')
|
|
||||||
let line = ''
|
|
||||||
let lineY = y
|
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
|
||||||
const testLine = line + words[i]
|
|
||||||
// 微信小程序中measureText返回的是对象,需要访问width属性
|
|
||||||
const metrics = ctx.measureText(testLine)
|
|
||||||
const testWidth = metrics.width
|
|
||||||
|
|
||||||
if (testWidth > maxWidth && i > 0) {
|
|
||||||
if (bold) {
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
ctx.fillText(line, x + 1, lineY)
|
|
||||||
ctx.fillText(line, x, lineY + 1)
|
|
||||||
ctx.fillText(line, x + 1, lineY + 1)
|
|
||||||
} else {
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
}
|
|
||||||
line = words[i]
|
|
||||||
lineY += fontSize * 1.2
|
|
||||||
} else {
|
|
||||||
line = testLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bold) {
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
ctx.fillText(line, x + 1, lineY)
|
|
||||||
ctx.fillText(line, x, lineY + 1)
|
|
||||||
ctx.fillText(line, x + 1, lineY + 1)
|
|
||||||
} else {
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制圆角矩形函数
|
// 绘制圆角矩形函数
|
||||||
@@ -156,22 +100,22 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
// 绘制标签函数(通用)
|
// 绘制标签函数(通用)
|
||||||
const drawLabel = (ctx: any, x: number, y: number, width: number, height: number, radius: number, text: string, fontSize: number, textColor: string = '#000000', bgColor: string = '#FFFFFF', borderColor: string = '#E0E0E0') => {
|
const drawLabel = (ctx: any, x: number, y: number, width: number, height: number, radius: number, text: string, fontSize: number, textColor: string = '#000000', bgColor: string = '#FFFFFF', borderColor: string = '#E0E0E0') => {
|
||||||
// 绘制背景
|
// 绘制背景
|
||||||
ctx.setFillStyle(bgColor)
|
ctx.fillStyle = bgColor
|
||||||
drawRoundedRect(ctx, x, y, width, height, radius)
|
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||||
|
|
||||||
// 绘制边框
|
// 绘制边框
|
||||||
ctx.setStrokeStyle(borderColor)
|
ctx.strokeStyle = borderColor
|
||||||
ctx.setLineWidth(1 * dpr)
|
ctx.lineWidth = 1 * dpr
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
const textCenterX = x + width / 2
|
const textCenterX = x + width / 2
|
||||||
const textCenterY = y + height / 2
|
const textCenterY = y + height / 2
|
||||||
|
|
||||||
ctx.setFillStyle(textColor)
|
ctx.fillStyle = textColor
|
||||||
ctx.setTextAlign('center')
|
ctx.textAlign = 'center'
|
||||||
ctx.setTextBaseline('middle')
|
ctx.textBaseline = 'middle'
|
||||||
ctx.setFontSize(fontSize)
|
setFont2D(ctx, fontSize)
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(textCenterX, textCenterY)
|
ctx.translate(textCenterX, textCenterY)
|
||||||
@@ -180,28 +124,35 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载图片 - 微信小程序版本
|
// 加载图片 - 微信小程序版本
|
||||||
const loadImage = (src: string): Promise<string> => {
|
const loadImage = (src: string, canvas?: any): Promise<any> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.getImageInfo({
|
if (!canvas || typeof canvas.createImage !== 'function') {
|
||||||
src: src,
|
reject(new Error('2D canvas is required to load images'))
|
||||||
success: (res) => resolve(res.path),
|
return
|
||||||
fail: reject
|
}
|
||||||
})
|
try {
|
||||||
|
const img = canvas.createImage()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = (e: any) => reject(e)
|
||||||
|
img.src = src
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制SVG路径到Canvas
|
// 绘制SVG路径到Canvas
|
||||||
const drawSVGPathToCanvas = (ctx: any) => {
|
const drawSVGPathToCanvas = (ctx: any) => {
|
||||||
// 设置绘制样式
|
// 设置绘制样式
|
||||||
ctx.setStrokeStyle('#48D800');
|
ctx.strokeStyle = '#00E5AD';
|
||||||
ctx.setLineWidth(scale * 3 * dpr);
|
ctx.lineWidth = scale * 3 * dpr;
|
||||||
ctx.setLineCap('round');
|
ctx.lineCap = 'round';
|
||||||
ctx.setLineJoin('round');
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// 移动到指定位置并缩放
|
// 移动到指定位置并缩放
|
||||||
ctx.translate(scale * 210 * dpr, scale * 90 * dpr);
|
ctx.translate(scale * 200 * dpr, scale * 90 * dpr);
|
||||||
const scaleValue = 0.8
|
const scaleValue = 0.8
|
||||||
ctx.scale(scaleValue, scaleValue);
|
ctx.scale(scaleValue, scaleValue);
|
||||||
|
|
||||||
@@ -226,7 +177,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 绘制右上角场地图片
|
// 绘制右上角场地图片
|
||||||
const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
const drawVenueImages = async (ctx: any, venueImageConfig: any, canvas?: any) => {
|
||||||
// 如果只有一张图
|
// 如果只有一张图
|
||||||
const playerImgX = venueImageConfig.venueImgX
|
const playerImgX = venueImageConfig.venueImgX
|
||||||
const playerImgY = venueImageConfig.venueImgY
|
const playerImgY = venueImageConfig.venueImgY
|
||||||
@@ -237,7 +188,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const venueImage = venueImageConfig.venueImage
|
const venueImage = venueImageConfig.venueImage
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const playerImgPath = await loadImage(venueImage)
|
const playerImgPath = await loadImage(venueImage, canvas)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
// 移动到旋转中心点
|
// 移动到旋转中心点
|
||||||
@@ -249,7 +200,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 1. 先绘制白色圆角矩形背景
|
// 1. 先绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
// 使用更精确的圆角矩形绘制
|
// 使用更精确的圆角矩形绘制
|
||||||
@@ -338,7 +289,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 绘制白色圆角矩形背景
|
// 绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
const rectX = -playerImgSize / 2
|
const rectX = -playerImgSize / 2
|
||||||
@@ -380,7 +331,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const imgY = -playerImgSize / 2 + padding
|
const imgY = -playerImgSize / 2 + padding
|
||||||
const imgSize = playerImgSize - padding * 2
|
const imgSize = playerImgSize - padding * 2
|
||||||
|
|
||||||
ctx.setFillStyle('#E0E0E0')
|
ctx.fillStyle = '#E0E0E0'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
const imgRadius = borderRadius - padding
|
const imgRadius = borderRadius - padding
|
||||||
|
|
||||||
@@ -448,15 +399,15 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
|
|
||||||
// 绘制背景 - 渐变色 已完成
|
// 绘制背景 - 渐变色 已完成
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
||||||
gradient.addColorStop(0, '#D8FFE5')
|
gradient.addColorStop(0, '#BFFFEF')
|
||||||
gradient.addColorStop(1, '#F9FFFB')
|
gradient.addColorStop(1, '#F2FFFC')
|
||||||
ctx.setFillStyle(gradient)
|
ctx.fillStyle = gradient
|
||||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||||
console.log('背景绘制完成')
|
console.log('背景绘制完成')
|
||||||
|
|
||||||
// 绘制背景条纹 已完成
|
// 绘制背景条纹 已完成
|
||||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
|
||||||
ctx.setLineWidth(2)
|
ctx.lineWidth = 2
|
||||||
for (let i = 0; i < canvasWidthPx; i += 4) {
|
for (let i = 0; i < canvasWidthPx; i += 4) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(i, 0)
|
ctx.moveTo(i, 0)
|
||||||
@@ -470,7 +421,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const avatarY = scale * 35 * dpr // 距离顶部35px
|
const avatarY = scale * 35 * dpr // 距离顶部35px
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const avatarPath = await loadImage(data.userAvatar)
|
const avatarPath = await loadImage(data.userAvatar, canvasNode)
|
||||||
// 微信小程序中绘制圆形头像需要特殊处理
|
// 微信小程序中绘制圆形头像需要特殊处理
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -480,7 +431,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果头像加载失败,绘制默认头像
|
// 如果头像加载失败,绘制默认头像
|
||||||
ctx.setFillStyle('#CCCCCC')
|
ctx.fillStyle = '#CCCCCC'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
|
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
@@ -490,7 +441,8 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
||||||
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
||||||
const nicknameFontSize = scale * 18 * dpr
|
const nicknameFontSize = scale * 18 * dpr
|
||||||
drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, '"Noto Sans SC"')
|
// drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, 'Noto Sans SC')
|
||||||
|
drawBoldText(ctx, data.userNickname, nicknameX, nicknameY, nicknameFontSize, '#000000', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 绘制"邀你加入球局"文案
|
// 绘制"邀你加入球局"文案
|
||||||
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
||||||
@@ -498,12 +450,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const inviteFontSize = scale * 44 * dpr
|
const inviteFontSize = scale * 44 * dpr
|
||||||
|
|
||||||
// 绘制"邀你加入"
|
// 绘制"邀你加入"
|
||||||
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', "Noto Sans SC")
|
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 绘制"球局"特殊样式
|
// 绘制"球局"特殊样式
|
||||||
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 5 * dpr
|
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
|
||||||
const qiuJuFontSize = scale * 44 * dpr
|
const qiuJuFontSize = scale * 44 * dpr
|
||||||
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#48D800', '"Noto Sans SC"')
|
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 测试绘制网络图片
|
// 测试绘制网络图片
|
||||||
drawSVGPathToCanvas(ctx)
|
drawSVGPathToCanvas(ctx)
|
||||||
@@ -528,16 +480,16 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
venueImgY: scale * 35 * dpr,
|
venueImgY: scale * 35 * dpr,
|
||||||
rotation: scale * -10, // 旋转-10度
|
rotation: scale * -10, // 旋转-10度
|
||||||
}
|
}
|
||||||
await drawVenueImages(ctx, venueBackConfig)
|
await drawVenueImages(ctx, venueBackConfig, canvasNode)
|
||||||
// 前面的图
|
// 前面的图
|
||||||
const venueFrontConfig = {
|
const venueFrontConfig = {
|
||||||
...venueBaseConfig,
|
...venueBaseConfig,
|
||||||
venueImage: data.venueImages?.[0],
|
venueImage: data.venueImages?.[0],
|
||||||
rotation: scale * 8, // 旋转-8度
|
rotation: scale * 8, // 旋转-8度
|
||||||
}
|
}
|
||||||
await drawVenueImages(ctx, venueFrontConfig)
|
await drawVenueImages(ctx, venueFrontConfig, canvasNode)
|
||||||
} else {
|
} else {
|
||||||
await drawVenueImages(ctx, venueBaseConfig)
|
await drawVenueImages(ctx, venueBaseConfig, canvasNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制球局信息区域
|
// 绘制球局信息区域
|
||||||
@@ -554,26 +506,35 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const textX = iconX + iconSize + 20
|
const textX = iconX + iconSize + 20
|
||||||
|
|
||||||
// 绘制网球图标
|
// 绘制网球图标
|
||||||
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`)
|
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
|
||||||
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
||||||
|
|
||||||
// 绘制"单打"标签
|
// 绘制"单打"标签
|
||||||
const danDaX = scale * 100
|
const danDaX = scale * 100
|
||||||
const danDaY = scale * 196
|
const danDaY = scale * 196
|
||||||
const danDaWidth = scale * 76 * dpr
|
|
||||||
const danDaHeight = scale * 40 * dpr
|
const danDaHeight = scale * 40 * dpr
|
||||||
const danDaRadius = scale * 20 * dpr
|
const danDaRadius = scale * 20 * dpr
|
||||||
const danDaFontSize = scale * 22 * dpr
|
const danDaFontSize = scale * 22 * dpr
|
||||||
|
// 根据内容动态计算标签宽度(左右内边距)
|
||||||
|
const danDaPaddingX = scale * 16 * dpr
|
||||||
|
setFont2D(ctx, danDaFontSize)
|
||||||
|
const danDaTextWidth = ctx.measureText(data.gameType).width
|
||||||
|
const danDaWidth = danDaTextWidth + danDaPaddingX * 2
|
||||||
|
|
||||||
drawLabel(ctx, danDaX, danDaY, danDaWidth, danDaHeight, danDaRadius, data.gameType, danDaFontSize)
|
drawLabel(ctx, danDaX, danDaY, danDaWidth, danDaHeight, danDaRadius, data.gameType, danDaFontSize)
|
||||||
|
|
||||||
// 绘制技能等级标签
|
// 绘制技能等级标签(基于“单打”标签实际宽度后移)
|
||||||
const skillX = scale * 190
|
const labelGap = scale * 16 // 两个标签之间的间距(不乘 dpr,保持视觉间距)
|
||||||
|
const skillX = danDaX + danDaWidth + labelGap
|
||||||
const skillY = scale * 196
|
const skillY = scale * 196
|
||||||
const skillWidth = scale * 180 * dpr
|
|
||||||
const skillHeight = scale * 40 * dpr
|
const skillHeight = scale * 40 * dpr
|
||||||
const skillRadius = scale * 20 * dpr
|
const skillRadius = scale * 20 * dpr
|
||||||
const skillFontSize = scale * 22 * dpr
|
const skillFontSize = scale * 22 * dpr
|
||||||
|
// 根据内容动态计算技能标签宽度
|
||||||
|
const skillPaddingX = scale * 20 * dpr
|
||||||
|
setFont2D(ctx, skillFontSize)
|
||||||
|
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
||||||
|
const skillWidth = skillTextWidth + skillPaddingX * 2
|
||||||
|
|
||||||
drawLabel(ctx, skillX, skillY, skillWidth, skillHeight, skillRadius, data.skillLevel, skillFontSize)
|
drawLabel(ctx, skillX, skillY, skillWidth, skillHeight, skillRadius, data.skillLevel, skillFontSize)
|
||||||
|
|
||||||
@@ -581,48 +542,56 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const dateX = danDaX
|
const dateX = danDaX
|
||||||
const timeInfoY = infoStartY + infoSpacing
|
const timeInfoY = infoStartY + infoSpacing
|
||||||
const timeInfoFontSize = scale * 24 * dpr
|
const timeInfoFontSize = scale * 24 * dpr
|
||||||
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
|
||||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
||||||
|
|
||||||
// 绘制日期(绿色)
|
// 绘制日期(绿色)
|
||||||
drawText(ctx, data.gameDate, dateX, timeInfoY + 8, 300, timeInfoFontSize, '#4CAF50')
|
drawBoldText(ctx, data.gameDate, dateX, timeInfoY + 8, timeInfoFontSize, '#00E5AD')
|
||||||
|
|
||||||
// 绘制时间(黑色)
|
// 绘制时间(黑色)
|
||||||
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
|
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
|
||||||
drawText(ctx, data.gameTime, timeX, timeInfoY + 8, 300, timeInfoFontSize, '#000000')
|
// drawText(ctx, data.gameTime, timeX, timeInfoY + 8, 300, timeInfoFontSize, '#000000')
|
||||||
|
drawBoldText(ctx, data.gameTime, timeX, timeInfoY + 8, timeInfoFontSize, '#000000')
|
||||||
|
|
||||||
// 绘制地点
|
// 绘制地点
|
||||||
const locationInfoY = infoStartY + infoSpacing * 2
|
const locationInfoY = infoStartY + infoSpacing * 2
|
||||||
const locationFontSize = scale * 22 * dpr
|
const locationFontSize = scale * 22 * dpr
|
||||||
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode)
|
||||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
||||||
drawText(ctx, data.venueName, danDaX, locationInfoY + 10, 600, locationFontSize, '#000000')
|
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
|
||||||
|
|
||||||
// 绘制完成,调用draw方法
|
// 绘制完成,调用draw方法
|
||||||
console.log('开始调用ctx.draw()')
|
console.log('开始调用ctx.draw()')
|
||||||
ctx.draw(false, () => {
|
const doExport = () => {
|
||||||
console.log('Canvas绘制完成,开始生成图片...')
|
console.log('Canvas绘制完成,开始生成图片...')
|
||||||
// 延迟一下再生成图片,确保绘制完成
|
const opts: any = {
|
||||||
setTimeout(() => {
|
fileType: 'png',
|
||||||
Taro.canvasToTempFilePath({
|
quality: 1,
|
||||||
canvasId: 'shareCardCanvas',
|
success: (res: any) => {
|
||||||
fileType: 'png',
|
console.log('图片生成成功:', res.tempFilePath)
|
||||||
quality: 1,
|
setIsDrawing(false)
|
||||||
success: (res) => {
|
resolve(res.tempFilePath)
|
||||||
console.log('图片生成成功:', res.tempFilePath)
|
onGenerated?.(res.tempFilePath)
|
||||||
setIsDrawing(false) // 绘制完成,重置状态
|
setTempImagePath(res.tempFilePath)
|
||||||
resolve(res.tempFilePath)
|
},
|
||||||
onGenerated?.(res.tempFilePath)
|
fail: (error: any) => {
|
||||||
setTempImagePath(res.tempFilePath)
|
console.error('图片生成失败:', error)
|
||||||
},
|
setIsDrawing(false)
|
||||||
fail: (error) => {
|
reject(error)
|
||||||
console.error('图片生成失败:', error)
|
}
|
||||||
setIsDrawing(false) // 绘制失败,重置状态
|
}
|
||||||
reject(error)
|
if (canvasNode) {
|
||||||
}
|
opts.canvas = canvasNode
|
||||||
})
|
} else {
|
||||||
}, 500) // 延迟500ms确保Canvas完全渲染
|
opts.canvasId = 'shareCardCanvas'
|
||||||
})
|
}
|
||||||
|
;(Taro as any).canvasToTempFilePath(opts)
|
||||||
|
}
|
||||||
|
if (typeof (ctx as any).draw === 'function') {
|
||||||
|
;(ctx as any).draw(false, () => setTimeout(doExport, 300))
|
||||||
|
} else {
|
||||||
|
setTimeout(doExport, 100)
|
||||||
|
}
|
||||||
console.log('Canvas绘制命令已发送')
|
console.log('Canvas绘制命令已发送')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -639,20 +608,79 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
|
|
||||||
// 手动分享方法(已移除,由父组件处理分享)
|
// 手动分享方法(已移除,由父组件处理分享)
|
||||||
|
|
||||||
|
// 使用 HTTPS 远程字体(woff2)加载到小程序渲染层;不改字号
|
||||||
// 组件挂载后绘制
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && !isDrawing && !tempImagePath) {
|
try {
|
||||||
console.log('组件挂载,开始绘制分享卡片')
|
(Taro as any).loadFontFace({
|
||||||
// 延迟一下确保Canvas已经渲染
|
global: true,
|
||||||
setTimeout(() => {
|
family: 'Noto Sans SC',
|
||||||
// 在微信小程序中,需要使用Taro.createCanvasContext
|
source: 'url("https://fonts.gstatic.com/s/notosanssc/v39/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaGzjCnYlNbPzT7HEL7j12XCOHJKg4RgZw3nFTvwZ8atTsBvwlvRUk7mYP2g.24.woff2")',
|
||||||
const ctx = Taro.createCanvasContext('shareCardCanvas')
|
desc: { style: 'normal', weight: '700' },
|
||||||
console.log('Canvas上下文创建成功:', ctx)
|
success: () => {
|
||||||
drawShareCard(ctx)
|
console.log('===Noto Sans SC 远程字体加载成功')
|
||||||
}, 500)
|
try {
|
||||||
|
if (ctx2d && is2dCtx) {
|
||||||
|
drawShareCard(ctx2d)
|
||||||
|
} else {
|
||||||
|
console.log('字体已加载,但 2D 上下文尚未就绪,等待初始化后再绘制')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('===字体加载成功但重绘失败(忽略)', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.warn('===Noto Sans SC 远程字体加载失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('===loadFontFace 不可用,跳过远程字体加载', e)
|
||||||
}
|
}
|
||||||
}, [data]) // 只依赖data,移除canvasWidth避免无限循环
|
}, [ctx2d, is2dCtx])
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化 2D Canvas(可回退旧版)
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
;(Taro as any).createSelectorQuery()
|
||||||
|
.select('#shareCardCanvas')
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res: any[]) => {
|
||||||
|
const data = res && res[0]
|
||||||
|
if (data && data.node) {
|
||||||
|
const canvas = data.node
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
// DPR 缩放,提升清晰度(当前 dpr = 1 也可正常显示)
|
||||||
|
const sys = (Taro as any).getSystemInfoSync?.() || {}
|
||||||
|
const ratio = sys.pixelRatio || 1
|
||||||
|
canvas.width = canvasWidth * ratio
|
||||||
|
canvas.height = canvasHeight * ratio
|
||||||
|
context.scale(ratio, ratio)
|
||||||
|
setCanvasNode(canvas)
|
||||||
|
setCtx2d(context)
|
||||||
|
setIs2dCtx(true)
|
||||||
|
console.log('2D Canvas 初始化成功')
|
||||||
|
} else {
|
||||||
|
setIs2dCtx(false)
|
||||||
|
console.log('2D Canvas 不可用,回退旧版 Canvas')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('初始化 2D Canvas 失败,回退旧版', e)
|
||||||
|
setIs2dCtx(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 组件挂载后绘制(仅在 2D 上下文就绪后绘制,避免回退导致 getImageInfo 404)
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && !isDrawing && !tempImagePath && ctx2d && is2dCtx) {
|
||||||
|
console.log('组件挂载,开始绘制分享卡片')
|
||||||
|
// 延迟一下确保Canvas已经渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('使用 2D Canvas 开始绘制')
|
||||||
|
drawShareCard(ctx2d)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}, [data, ctx2d, is2dCtx]) // 等 2D 上下文可用后再绘制
|
||||||
|
|
||||||
// 暴露分享方法给父组件
|
// 暴露分享方法给父组件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -666,10 +694,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
<View className={`share-card-canvas ${className}`}>
|
<View className={`share-card-canvas ${className}`}>
|
||||||
<Canvas
|
<Canvas
|
||||||
canvasId="shareCardCanvas"
|
canvasId="shareCardCanvas"
|
||||||
|
id="shareCardCanvas"
|
||||||
|
type="2d"
|
||||||
style={{
|
style={{
|
||||||
width: `${canvasWidth}px`,
|
width: `${canvasWidth}px`,
|
||||||
height: `${canvasHeight}px`,
|
height: `${canvasHeight}px`,
|
||||||
position: 'absolute', // 绝对定位避免影响布局
|
// position: 'absolute', // 绝对定位避免影响布局
|
||||||
// top: '-9999px', // 移出可视区域
|
// top: '-9999px', // 移出可视区域
|
||||||
// left: '-9999px'
|
// left: '-9999px'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -43,120 +43,25 @@ const runtime: {
|
|||||||
offscreen?: any
|
offscreen?: any
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// 兼容适配:将微信 CanvasContext 的 set* 方法映射到标准 Canvas 2D 属性
|
// 2D Canvas 字体设置
|
||||||
const polyfillCanvasContext = (ctx: any) => {
|
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
|
||||||
if (ctx && typeof ctx === 'object') {
|
const fam = family || 'Noto Sans SC'
|
||||||
if (typeof ctx.setFillStyle !== 'function') {
|
const wt = weight || '500'
|
||||||
ctx.setFillStyle = (v: any) => { ctx.fillStyle = v }
|
ctx.font = `${wt} ${fontSize}px "${fam}"`
|
||||||
}
|
|
||||||
if (typeof ctx.setStrokeStyle !== 'function') {
|
|
||||||
ctx.setStrokeStyle = (v: any) => { ctx.strokeStyle = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineWidth !== 'function') {
|
|
||||||
ctx.setLineWidth = (v: number) => { ctx.lineWidth = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineCap !== 'function') {
|
|
||||||
ctx.setLineCap = (v: any) => { ctx.lineCap = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineJoin !== 'function') {
|
|
||||||
ctx.setLineJoin = (v: any) => { ctx.lineJoin = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setTextAlign !== 'function') {
|
|
||||||
ctx.setTextAlign = (v: any) => { ctx.textAlign = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setTextBaseline !== 'function') {
|
|
||||||
ctx.setTextBaseline = (v: any) => { ctx.textBaseline = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setFontSize !== 'function') {
|
|
||||||
ctx.setFontSize = (size: number) => { ctx.font = `${size}px sans-serif` }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setFont !== 'function') {
|
|
||||||
ctx.setFont = (fontStr: string) => { ctx.font = fontStr }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制加粗文字(单行)
|
// 绘制加粗文字(单行,支持可选描边式加粗)
|
||||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily?: string) => {
|
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily: string = 'Noto Sans SC', fontWeight: string = '500') => {
|
||||||
// 设置字体样式
|
setFont2D(ctx, fontSize, fontFamily, fontWeight)
|
||||||
if (fontFamily) {
|
ctx.fillStyle = color
|
||||||
try {
|
ctx.textAlign = 'left'
|
||||||
// 尝试使用setFont方法(如果支持)
|
ctx.textBaseline = 'top'
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
|
||||||
ctx.setTextAlign('left')
|
|
||||||
ctx.setTextBaseline('top')
|
|
||||||
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(text, x, y)
|
ctx.fillText(text, x, y)
|
||||||
ctx.fillText(text, x + 1, y)
|
// if (isBold) {
|
||||||
ctx.fillText(text, x, y + 1)
|
// ctx.fillText(text, x + 1, y)
|
||||||
ctx.fillText(text, x + 1, y + 1)
|
// ctx.fillText(text, x, y + 1)
|
||||||
}
|
// ctx.fillText(text, x + 1, y + 1)
|
||||||
|
// }
|
||||||
// 绘制文字(支持自动换行)- 微信小程序版本
|
|
||||||
const drawText = (ctx: any, text: string, x: number, y: number, maxWidth: number, fontSize: number, color: string, bold: boolean = false, fontFamily?: string) => {
|
|
||||||
// 设置字体样式
|
|
||||||
if (fontFamily) {
|
|
||||||
try {
|
|
||||||
// 尝试使用setFont方法(如果支持)
|
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
|
||||||
ctx.setTextAlign('left')
|
|
||||||
ctx.setTextBaseline('top')
|
|
||||||
|
|
||||||
const words = text.split('')
|
|
||||||
let line = ''
|
|
||||||
let lineY = y
|
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
|
||||||
const testLine = line + words[i]
|
|
||||||
// 微信小程序中measureText返回的是对象,需要访问width属性
|
|
||||||
const metrics = ctx.measureText(testLine)
|
|
||||||
const testWidth = metrics.width
|
|
||||||
|
|
||||||
if (testWidth > maxWidth && i > 0) {
|
|
||||||
if (bold) {
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
ctx.fillText(line, x + 1, lineY)
|
|
||||||
ctx.fillText(line, x, lineY + 1)
|
|
||||||
ctx.fillText(line, x + 1, lineY + 1)
|
|
||||||
} else {
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
}
|
|
||||||
line = words[i]
|
|
||||||
lineY += fontSize * 1.2
|
|
||||||
} else {
|
|
||||||
line = testLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bold) {
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
ctx.fillText(line, x + 1, lineY)
|
|
||||||
ctx.fillText(line, x, lineY + 1)
|
|
||||||
ctx.fillText(line, x + 1, lineY + 1)
|
|
||||||
} else {
|
|
||||||
ctx.fillText(line, x, lineY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制圆角矩形函数
|
// 绘制圆角矩形函数
|
||||||
@@ -178,22 +83,22 @@ const drawRoundedRect = (ctx: any, x: number, y: number, width: number, height:
|
|||||||
// 绘制标签函数(通用)
|
// 绘制标签函数(通用)
|
||||||
const drawLabel = (ctx: any, x: number, y: number, width: number, height: number, radius: number, text: string, fontSize: number, textColor: string = '#000000', bgColor: string = '#FFFFFF', borderColor: string = '#E0E0E0') => {
|
const drawLabel = (ctx: any, x: number, y: number, width: number, height: number, radius: number, text: string, fontSize: number, textColor: string = '#000000', bgColor: string = '#FFFFFF', borderColor: string = '#E0E0E0') => {
|
||||||
// 绘制背景
|
// 绘制背景
|
||||||
ctx.setFillStyle(bgColor)
|
ctx.fillStyle = bgColor
|
||||||
drawRoundedRect(ctx, x, y, width, height, radius)
|
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||||
|
|
||||||
// 绘制边框
|
// 绘制边框
|
||||||
ctx.setStrokeStyle(borderColor)
|
ctx.strokeStyle = borderColor
|
||||||
ctx.setLineWidth(1 * dpr)
|
ctx.lineWidth = 1 * dpr
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
const textCenterX = x + width / 2
|
const textCenterX = x + width / 2
|
||||||
const textCenterY = y + height / 2
|
const textCenterY = y + height / 2
|
||||||
|
|
||||||
ctx.setFillStyle(textColor)
|
ctx.fillStyle = textColor
|
||||||
ctx.setTextAlign('center')
|
ctx.textAlign = 'center'
|
||||||
ctx.setTextBaseline('middle')
|
ctx.textBaseline = 'middle'
|
||||||
ctx.setFontSize(fontSize)
|
setFont2D(ctx, fontSize)
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(textCenterX, textCenterY)
|
ctx.translate(textCenterX, textCenterY)
|
||||||
@@ -201,49 +106,31 @@ const drawLabel = (ctx: any, x: number, y: number, width: number, height: number
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载图片 - 微信小程序版本
|
// 工具函数 - OffscreenCanvas 下加载图片(使用 offscreen.createImage)
|
||||||
// const loadImage = (src: string): Promise<string> => {
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// Taro.getImageInfo({
|
|
||||||
// src: src,
|
|
||||||
// success: (res) => resolve(res.path),
|
|
||||||
// fail: reject
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 工具函数 - OffscreenCanvas 下加载图片(从 runtime.offscreen 读取)
|
|
||||||
const loadImage = (src: string): Promise<any> => {
|
const loadImage = (src: string): Promise<any> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.getImageInfo({
|
try {
|
||||||
src,
|
const off = runtime.offscreen
|
||||||
success: (res) => {
|
if (!off || typeof off.createImage !== 'function') {
|
||||||
try {
|
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
||||||
// @ts-ignore - createImage 为小程序 OffscreenCanvas 能力
|
}
|
||||||
const off = runtime.offscreen
|
const img = off.createImage()
|
||||||
if (!off || typeof off.createImage !== 'function') {
|
img.onload = () => resolve(img)
|
||||||
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
img.onerror = reject
|
||||||
}
|
img.src = src
|
||||||
const img = off.createImage()
|
} catch (e) {
|
||||||
img.onload = () => resolve(img)
|
reject(e)
|
||||||
img.onerror = reject
|
}
|
||||||
img.src = res.path
|
|
||||||
} catch (e) {
|
|
||||||
reject(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: reject
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制SVG路径到Canvas
|
// 绘制SVG路径到Canvas
|
||||||
const drawSVGPathToCanvas = (ctx: any) => {
|
const drawSVGPathToCanvas = (ctx: any) => {
|
||||||
// 设置绘制样式
|
// 设置绘制样式
|
||||||
ctx.setStrokeStyle('#00E5AD');
|
ctx.strokeStyle = '#00E5AD';
|
||||||
ctx.setLineWidth(scale * 3 * dpr);
|
ctx.lineWidth = scale * 3 * dpr;
|
||||||
ctx.setLineCap('round');
|
ctx.lineCap = 'round';
|
||||||
ctx.setLineJoin('round');
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
@@ -296,7 +183,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 1. 先绘制白色圆角矩形背景
|
// 1. 先绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
// 使用更精确的圆角矩形绘制
|
// 使用更精确的圆角矩形绘制
|
||||||
@@ -385,7 +272,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 绘制白色圆角矩形背景
|
// 绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
const rectX = -playerImgSize / 2
|
const rectX = -playerImgSize / 2
|
||||||
@@ -427,7 +314,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
const imgY = -playerImgSize / 2 + padding
|
const imgY = -playerImgSize / 2 + padding
|
||||||
const imgSize = playerImgSize - padding * 2
|
const imgSize = playerImgSize - padding * 2
|
||||||
|
|
||||||
ctx.setFillStyle('#E0E0E0')
|
ctx.fillStyle = '#E0E0E0'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
const imgRadius = borderRadius - padding
|
const imgRadius = borderRadius - padding
|
||||||
|
|
||||||
@@ -471,8 +358,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
console.log('开始绘制分享卡片...')
|
console.log('开始绘制分享卡片...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先对 2D 上下文做一次 API 兼容处理
|
|
||||||
polyfillCanvasContext(ctx)
|
|
||||||
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
||||||
const canvasWidthPx = canvasWidth * dpr
|
const canvasWidthPx = canvasWidth * dpr
|
||||||
const canvasHeightPx = canvasHeight * dpr
|
const canvasHeightPx = canvasHeight * dpr
|
||||||
@@ -492,13 +377,13 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
||||||
gradient.addColorStop(0, '#BFFFEF')
|
gradient.addColorStop(0, '#BFFFEF')
|
||||||
gradient.addColorStop(1, '#F2FFFC')
|
gradient.addColorStop(1, '#F2FFFC')
|
||||||
ctx.setFillStyle(gradient)
|
ctx.fillStyle = gradient
|
||||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||||
console.log('背景绘制完成')
|
console.log('背景绘制完成')
|
||||||
|
|
||||||
// 绘制背景条纹 已完成
|
// 绘制背景条纹 已完成
|
||||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
|
||||||
ctx.setLineWidth(2)
|
ctx.lineWidth = 2
|
||||||
for (let i = 0; i < canvasWidthPx; i += 4) {
|
for (let i = 0; i < canvasWidthPx; i += 4) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(i, 0)
|
ctx.moveTo(i, 0)
|
||||||
@@ -532,7 +417,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
||||||
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
||||||
const nicknameFontSize = scale * 18 * dpr
|
const nicknameFontSize = scale * 18 * dpr
|
||||||
drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, '"Noto Sans SC"')
|
drawBoldText(ctx, data.userNickname, nicknameX, nicknameY, nicknameFontSize, '#000000', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 绘制"邀你加入球局"文案
|
// 绘制"邀你加入球局"文案
|
||||||
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
||||||
@@ -540,12 +425,12 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const inviteFontSize = scale * 44 * dpr
|
const inviteFontSize = scale * 44 * dpr
|
||||||
|
|
||||||
// 绘制"邀你加入"
|
// 绘制"邀你加入"
|
||||||
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', "Noto Sans SC")
|
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 绘制"球局"特殊样式
|
// 绘制"球局"特殊样式
|
||||||
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
|
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
|
||||||
const qiuJuFontSize = scale * 44 * dpr
|
const qiuJuFontSize = scale * 44 * dpr
|
||||||
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', '"Noto Sans SC"')
|
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 测试绘制网络图片
|
// 测试绘制网络图片
|
||||||
drawSVGPathToCanvas(ctx)
|
drawSVGPathToCanvas(ctx)
|
||||||
@@ -607,7 +492,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const danDaFontSize = scale * 22 * dpr
|
const danDaFontSize = scale * 22 * dpr
|
||||||
// 根据内容动态计算标签宽度(左右内边距)
|
// 根据内容动态计算标签宽度(左右内边距)
|
||||||
const danDaPaddingX = scale * 16 * dpr
|
const danDaPaddingX = scale * 16 * dpr
|
||||||
ctx.setFontSize(danDaFontSize)
|
setFont2D(ctx, danDaFontSize)
|
||||||
const danDaTextWidth = ctx.measureText(data.gameType).width
|
const danDaTextWidth = ctx.measureText(data.gameType).width
|
||||||
const danDaWidth = danDaTextWidth + danDaPaddingX * 2
|
const danDaWidth = danDaTextWidth + danDaPaddingX * 2
|
||||||
|
|
||||||
@@ -622,7 +507,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const skillFontSize = scale * 22 * dpr
|
const skillFontSize = scale * 22 * dpr
|
||||||
// 根据内容动态计算技能标签宽度
|
// 根据内容动态计算技能标签宽度
|
||||||
const skillPaddingX = scale * 20 * dpr
|
const skillPaddingX = scale * 20 * dpr
|
||||||
ctx.setFontSize(skillFontSize)
|
setFont2D(ctx, skillFontSize)
|
||||||
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
||||||
const skillWidth = skillTextWidth + skillPaddingX * 2
|
const skillWidth = skillTextWidth + skillPaddingX * 2
|
||||||
|
|
||||||
@@ -635,19 +520,19 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
||||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
||||||
|
|
||||||
// 绘制日期(绿色)
|
// 绘制日期(绿色,非描边粗体)
|
||||||
drawText(ctx, data.gameDate, dateX, timeInfoY + 8, 300, timeInfoFontSize, '#00E5AD')
|
drawBoldText(ctx, data.gameDate, dateX, timeInfoY + 8, timeInfoFontSize, '#00E5AD')
|
||||||
|
|
||||||
// 绘制时间(黑色)
|
// 绘制时间(黑色)
|
||||||
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
|
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
|
||||||
drawText(ctx, data.gameTime, timeX, timeInfoY + 8, 300, timeInfoFontSize, '#000000')
|
drawBoldText(ctx, data.gameTime, timeX, timeInfoY + 8, timeInfoFontSize, '#000000')
|
||||||
|
|
||||||
// 绘制地点
|
// 绘制地点
|
||||||
const locationInfoY = infoStartY + infoSpacing * 2
|
const locationInfoY = infoStartY + infoSpacing * 2
|
||||||
const locationFontSize = scale * 22 * dpr
|
const locationFontSize = scale * 22 * dpr
|
||||||
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
||||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
||||||
drawText(ctx, data.venueName, danDaX, locationInfoY + 10, 600, locationFontSize, '#000000')
|
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||||
|
|||||||
Reference in New Issue
Block a user