This commit is contained in:
2026-06-14 10:01:47 +08:00
parent 6fbf610277
commit ca39190ad7
11 changed files with 556 additions and 13 deletions
+140
View File
@@ -0,0 +1,140 @@
/**
* 生成好友之树分享图(Canvas 绘制,可下载发到微信朋友圈)
*/
export interface ShareTreeData {
friendName: string
myName: string
stageIndex: number
stageName: string
stageEmoji: string
messageCount: number
waterCount: number
totalScore: number
hue: number
}
function hsl(h: number, s: number, l: number): string {
return `hsl(${h}, ${s}%, ${l}%)`
}
/** 把树画到 canvas 上 */
function drawTree(ctx: CanvasRenderingContext2D, cx: number, baseY: number, stageIndex: number, hue: number) {
if (stageIndex === 0) {
// 种子
ctx.fillStyle = hsl(25, 45, 40)
ctx.beginPath()
ctx.ellipse(cx, baseY, 14, 10, 0, 0, Math.PI * 2)
ctx.fill()
return
}
const trunkW = 4 + stageIndex * 2.2
const canopyR = 22 + stageIndex * 9
const canopyN = 3 + stageIndex
// 树干
ctx.fillStyle = hsl(25, 45, 35 + stageIndex * 2)
ctx.fillRect(cx - trunkW / 2, baseY - 90, trunkW, 90)
// 树冠(多个圆叠加)
ctx.fillStyle = hsl(hue, 55, 42)
ctx.beginPath()
ctx.arc(cx, baseY - 100, canopyR, 0, Math.PI * 2)
ctx.fill()
for (let i = 1; i < canopyN; i++) {
const offset = i % 2 === 1 ? 1 : -1
const layer = Math.ceil(i / 2)
ctx.fillStyle = hsl(hue, 55, 42 + (i % 2) * 6)
ctx.beginPath()
ctx.arc(
cx + offset * canopyR * 0.6 * layer,
baseY - 100 + layer * canopyR * 0.3,
canopyR * (0.75 - layer * 0.1),
0, Math.PI * 2
)
ctx.fill()
}
// 高光
ctx.fillStyle = hsl(hue, 60, 60)
ctx.globalAlpha = 0.3
ctx.beginPath()
ctx.arc(cx - canopyR * 0.3, baseY - 100 - canopyR * 0.3, canopyR * 0.3, 0, Math.PI * 2)
ctx.fill()
ctx.globalAlpha = 1
}
/** 生成完整分享卡片,返回 dataURL */
export function generateTreeShareCard(data: ShareTreeData): string {
const W = 600, H = 800
const canvas = document.createElement('canvas')
canvas.width = W
canvas.height = H
const ctx = canvas.getContext('2d')!
// 背景渐变
const bg = ctx.createLinearGradient(0, 0, 0, H)
bg.addColorStop(0, '#E8F5E9')
bg.addColorStop(0.5, '#C8E6C9')
bg.addColorStop(1, '#A5D6A7')
ctx.fillStyle = bg
ctx.fillRect(0, 0, W, H)
// 顶部光晕
const glow = ctx.createRadialGradient(W / 2, 200, 0, W / 2, 200, 300)
glow.addColorStop(0, 'rgba(255,255,255,0.5)')
glow.addColorStop(1, 'rgba(255,255,255,0)')
ctx.fillStyle = glow
ctx.fillRect(0, 0, W, 400)
// logo + 标题
ctx.fillStyle = '#004D40'
ctx.font = 'bold 28px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif'
ctx.textAlign = 'center'
ctx.fillText('🌿 我们在青叶种了一棵树', W / 2, 70)
// 名字
ctx.font = '20px -apple-system, "PingFang SC", sans-serif'
ctx.fillStyle = '#00695C'
ctx.fillText(`${data.myName} × ${data.friendName}`, W / 2, 105)
// 地面
ctx.fillStyle = 'rgba(139,90,43,0.15)'
ctx.beginPath()
ctx.ellipse(W / 2, 560, 140, 16, 0, 0, Math.PI * 2)
ctx.fill()
// 画树
drawTree(ctx, W / 2, 560, data.stageIndex, data.hue)
// 阶段标签
ctx.font = 'bold 22px sans-serif'
ctx.fillStyle = '#00796B'
ctx.fillText(`${data.stageEmoji} ${data.stageName} Lv.${data.stageIndex + 1}`, W / 2, 620)
// 统计三栏
const stats = [
{ label: '条消息', value: data.messageCount },
{ label: '次浇水', value: data.waterCount },
{ label: '成长值', value: data.totalScore },
]
const statY = 670
stats.forEach((s, i) => {
const x = W / 4 + (W / 4) * i - 30
ctx.fillStyle = '#009688'
ctx.font = 'bold 26px sans-serif'
ctx.fillText(String(s.value), x, statY)
ctx.fillStyle = '#5F7A74'
ctx.font = '13px sans-serif'
ctx.fillText(s.label, x, statY + 22)
})
// 底部邀请
ctx.fillStyle = '#004D40'
ctx.font = '15px sans-serif'
ctx.fillText('来青叶,和你在意的人一起种树', W / 2, 745)
ctx.font = '12px sans-serif'
ctx.fillStyle = '#5F7A74'
ctx.fillText('微信给不了你的,是一片会生长的关系', W / 2, 770)
return canvas.toDataURL('image/png')
}