This commit is contained in:
2026-06-13 17:57:43 +08:00
parent 68678304ff
commit a0f441d8ae
28 changed files with 1933 additions and 2 deletions
+89
View File
@@ -0,0 +1,89 @@
/**
* 程序化叶子生成器
* 由确定性种子(hash(userId+date))派生独一无二的叶子形态
*/
export interface LeafStyle {
hue: number // 色相 0-360
saturation: number // 饱和度
lightness: number // 亮度
shapeVariant: number // 形态变体 0-3(不同叶形 path)
veinCount: number // 叶脉数量 3-7
angle: number // 叶子倾斜角度
spots: number // 斑点数量
size: number // 相对大小 0.85-1.1
}
/** 从 16 进制种子派生叶子样式 */
export function generateLeafStyle(seed: string): LeafStyle {
// 将 16 位 hex 种子拆成多段用于不同属性
const n = (start: number, len: number) => parseInt(seed.slice(start, start + len), 16)
const hue = n(0, 3) % 80 + 70 // 70-150 区间:黄绿到青绿
const saturation = 40 + (n(3, 2) % 35) // 40-75
const lightness = 45 + (n(5, 2) % 20) // 45-65
const shapeVariant = n(7, 1) % 4
const veinCount = 3 + (n(8, 1) % 5)
const angle = (n(9, 2) % 30) - 15 // -15 到 +15 度
const spots = n(11, 1) % 4
const size = 0.85 + (n(12, 2) % 26) / 100
return { hue, saturation, lightness, shapeVariant, veinCount, angle, spots, size }
}
export function leafColor(style: LeafStyle, lightDelta = 0): string {
return `hsl(${style.hue}, ${style.saturation}%, ${style.lightness + lightDelta}%)`
}
/** 4 种叶形 SVG pathviewBox 0 0 100 120 */
const LEAF_SHAPES = [
// 经典椭圆叶
'M50 8 C72 18 82 45 78 78 C74 104 60 116 50 116 C40 116 26 104 22 78 C18 45 28 18 50 8 Z',
// 心形叶
'M50 12 C60 4 78 8 80 28 C82 52 64 78 50 116 C36 78 18 52 20 28 C22 8 40 4 50 12 Z',
// 长披针叶
'M50 6 C62 30 66 60 62 92 C58 110 54 118 50 118 C46 118 42 110 38 92 C34 60 38 30 50 6 Z',
// 枫叶状
'M50 8 C58 22 56 30 68 34 C78 38 72 50 66 54 C74 62 70 74 58 72 C56 90 54 108 50 118 C46 108 44 90 42 72 C30 74 26 62 34 54 C28 50 22 38 32 34 C44 30 42 22 50 8 Z',
]
export function leafPath(variant: number): string {
return LEAF_SHAPES[variant % LEAF_SHAPES.length]
}
/** 生成叶脉 path(沿中线对称分支) */
export function veinPaths(style: LeafStyle): string[] {
const paths: string[] = []
// 主脉
paths.push('M50 16 L50 112')
// 侧脉
for (let i = 0; i < style.veinCount; i++) {
const y = 28 + i * (70 / style.veinCount)
const len = 16 + (i % 2) * 6
paths.push(`M50 ${y} Q38 ${y + 8} ${50 - len} ${y + 16}`)
paths.push(`M50 ${y} Q62 ${y + 8} ${50 + len} ${y + 16}`)
}
return paths
}
/** 生成斑点坐标(装饰) */
export function spotPositions(style: LeafStyle): { x: number; y: number; r: number }[] {
const spots: { x: number; y: number; r: number }[] = []
let s = parseInt(seedHash(style), 10) || 1
const rand = () => {
s = (s * 9301 + 49297) % 233280
return s / 233280
}
for (let i = 0; i < style.spots; i++) {
spots.push({
x: 30 + rand() * 40,
y: 30 + rand() * 60,
r: 1.5 + rand() * 2,
})
}
return spots
}
function seedHash(style: LeafStyle): string {
return '' + (style.hue * 7 + style.veinCount * 13 + style.spots * 31)
}
+77
View File
@@ -0,0 +1,77 @@
/**
* 程序化树生成器
* 根据阶段索引 + 种子渲染不同形态的树(SVG)
*/
export interface TreeStyle {
trunkWidth: number
canopyRadius: number
canopyCount: number
height: number
hue: number
dropletCount: number // 露珠数 = 浇水相关
}
/** 根据阶段和分数生成树样式 */
export function generateTreeStyle(stageIndex: number, seed: string, waterCount: number): TreeStyle {
const n = (start: number) => parseInt(seed.slice(start, start + 2) || '0', 16)
const baseCanopy = 18 + stageIndex * 10
const baseTrunk = 4 + stageIndex * 1.5
const hue = 90 + (n(0) % 40) // 90-130 绿色系
return {
trunkWidth: baseTrunk,
canopyRadius: baseCanopy,
canopyCount: 3 + stageIndex, // 树冠数量随阶段增加
height: 80 + stageIndex * 15,
hue,
dropletCount: Math.min(waterCount, 6),
}
}
export function treeTrunkColor(style: TreeStyle): string {
return `hsl(25, 45%, ${35 + style.trunkWidth}%)`
}
export function treeCanopyColor(style: TreeStyle, delta = 0): string {
return `hsl(${style.hue}, 55%, ${42 + delta}%)`
}
/** 生成树冠圆圈位置(围绕顶部) */
export function canopyPositions(style: TreeStyle, centerX: number, topY: number): { cx: number; cy: number; r: number }[] {
const positions: { cx: number; cy: number; r: number }[] = []
const count = style.canopyCount
// 主树冠
positions.push({ cx: centerX, cy: topY, r: style.canopyRadius })
// 侧边树冠
for (let i = 1; i < count; i++) {
const offset = i % 2 === 1 ? 1 : -1
const layer = Math.ceil(i / 2)
positions.push({
cx: centerX + offset * (style.canopyRadius * 0.6 * layer),
cy: topY + layer * style.canopyRadius * 0.3,
r: style.canopyRadius * (0.75 - layer * 0.1),
})
}
return positions
}
/** 生成露珠位置(挂在树冠上) */
export function dropletPositions(style: TreeStyle, centerX: number, topY: number): { x: number; y: number }[] {
const droplets: { x: number; y: number }[] = []
const positions = canopyPositions(style, centerX, topY)
let seed = parseInt(style.hue.toString() + style.dropletCount, 10) || 1
const rand = () => {
seed = (seed * 9301 + 49297) % 233280
return seed / 233280
}
for (let i = 0; i < style.dropletCount && i < positions.length; i++) {
const p = positions[i]
droplets.push({
x: p.cx + (rand() - 0.5) * p.r,
y: p.cy + (rand() - 0.5) * p.r * 0.6,
})
}
return droplets
}