处理分享canvas字体及加粗
This commit is contained in:
@@ -43,120 +43,25 @@ const runtime: {
|
||||
offscreen?: any
|
||||
} = {}
|
||||
|
||||
// 兼容适配:将微信 CanvasContext 的 set* 方法映射到标准 Canvas 2D 属性
|
||||
const polyfillCanvasContext = (ctx: any) => {
|
||||
if (ctx && typeof ctx === 'object') {
|
||||
if (typeof ctx.setFillStyle !== 'function') {
|
||||
ctx.setFillStyle = (v: any) => { ctx.fillStyle = v }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
// 2D Canvas 字体设置
|
||||
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
|
||||
const fam = family || 'Noto Sans SC'
|
||||
const wt = weight || '500'
|
||||
ctx.font = `${wt} ${fontSize}px "${fam}"`
|
||||
}
|
||||
|
||||
// 绘制加粗文字(单行)
|
||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, 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 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)
|
||||
ctx.fillStyle = color
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillText(text, x, y)
|
||||
ctx.fillText(text, x + 1, y)
|
||||
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)
|
||||
}
|
||||
// if (isBold) {
|
||||
// ctx.fillText(text, x + 1, y)
|
||||
// ctx.fillText(text, x, y + 1)
|
||||
// ctx.fillText(text, x + 1, y + 1)
|
||||
// }
|
||||
}
|
||||
|
||||
// 绘制圆角矩形函数
|
||||
@@ -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') => {
|
||||
// 绘制背景
|
||||
ctx.setFillStyle(bgColor)
|
||||
ctx.fillStyle = bgColor
|
||||
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||
|
||||
// 绘制边框
|
||||
ctx.setStrokeStyle(borderColor)
|
||||
ctx.setLineWidth(1 * dpr)
|
||||
ctx.strokeStyle = borderColor
|
||||
ctx.lineWidth = 1 * dpr
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制文字
|
||||
const textCenterX = x + width / 2
|
||||
const textCenterY = y + height / 2
|
||||
|
||||
ctx.setFillStyle(textColor)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.setTextBaseline('middle')
|
||||
ctx.setFontSize(fontSize)
|
||||
ctx.fillStyle = textColor
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
setFont2D(ctx, fontSize)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(textCenterX, textCenterY)
|
||||
@@ -201,49 +106,31 @@ const drawLabel = (ctx: any, x: number, y: number, width: number, height: number
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 加载图片 - 微信小程序版本
|
||||
// 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 读取)
|
||||
// 工具函数 - OffscreenCanvas 下加载图片(使用 offscreen.createImage)
|
||||
const loadImage = (src: string): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
try {
|
||||
// @ts-ignore - createImage 为小程序 OffscreenCanvas 能力
|
||||
const off = runtime.offscreen
|
||||
if (!off || typeof off.createImage !== 'function') {
|
||||
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
||||
}
|
||||
const img = off.createImage()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = res.path
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
try {
|
||||
const off = runtime.offscreen
|
||||
if (!off || typeof off.createImage !== 'function') {
|
||||
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
||||
}
|
||||
const img = off.createImage()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制SVG路径到Canvas
|
||||
const drawSVGPathToCanvas = (ctx: any) => {
|
||||
// 设置绘制样式
|
||||
ctx.setStrokeStyle('#00E5AD');
|
||||
ctx.setLineWidth(scale * 3 * dpr);
|
||||
ctx.setLineCap('round');
|
||||
ctx.setLineJoin('round');
|
||||
ctx.strokeStyle = '#00E5AD';
|
||||
ctx.lineWidth = scale * 3 * dpr;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
ctx.save();
|
||||
|
||||
@@ -296,7 +183,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
||||
ctx.rotate((rotation * Math.PI) / 180)
|
||||
|
||||
// 1. 先绘制白色圆角矩形背景
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
|
||||
// 使用更精确的圆角矩形绘制
|
||||
@@ -385,7 +272,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
||||
ctx.rotate((rotation * Math.PI) / 180)
|
||||
|
||||
// 绘制白色圆角矩形背景
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
|
||||
const rectX = -playerImgSize / 2
|
||||
@@ -427,7 +314,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
||||
const imgY = -playerImgSize / 2 + padding
|
||||
const imgSize = playerImgSize - padding * 2
|
||||
|
||||
ctx.setFillStyle('#E0E0E0')
|
||||
ctx.fillStyle = '#E0E0E0'
|
||||
ctx.beginPath()
|
||||
const imgRadius = borderRadius - padding
|
||||
|
||||
@@ -471,8 +358,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
console.log('开始绘制分享卡片...')
|
||||
|
||||
try {
|
||||
// 先对 2D 上下文做一次 API 兼容处理
|
||||
polyfillCanvasContext(ctx)
|
||||
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
||||
const canvasWidthPx = canvasWidth * 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)
|
||||
gradient.addColorStop(0, '#BFFFEF')
|
||||
gradient.addColorStop(1, '#F2FFFC')
|
||||
ctx.setFillStyle(gradient)
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||
console.log('背景绘制完成')
|
||||
|
||||
// 绘制背景条纹 已完成
|
||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
|
||||
ctx.lineWidth = 2
|
||||
for (let i = 0; i < canvasWidthPx; i += 4) {
|
||||
ctx.beginPath()
|
||||
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 nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
||||
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
|
||||
@@ -540,12 +425,12 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
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 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)
|
||||
@@ -607,7 +492,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
const danDaFontSize = scale * 22 * dpr
|
||||
// 根据内容动态计算标签宽度(左右内边距)
|
||||
const danDaPaddingX = scale * 16 * dpr
|
||||
ctx.setFontSize(danDaFontSize)
|
||||
setFont2D(ctx, danDaFontSize)
|
||||
const danDaTextWidth = ctx.measureText(data.gameType).width
|
||||
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 skillPaddingX = scale * 20 * dpr
|
||||
ctx.setFontSize(skillFontSize)
|
||||
setFont2D(ctx, skillFontSize)
|
||||
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
||||
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`)
|
||||
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
|
||||
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 locationFontSize = scale * 22 * dpr
|
||||
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
||||
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 {
|
||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
|
||||
Reference in New Issue
Block a user