🎉 Initial commit

This commit is contained in:
2025-03-19 21:10:19 +08:00
commit 86c8755f79
70 changed files with 6915 additions and 0 deletions

741
pixelnebula.go Normal file
View File

@@ -0,0 +1,741 @@
package pixelnebula
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"log"
"os"
"regexp"
"strconv"
"strings"
"github.com/landaiqing/go-pixelnebula/animation"
"github.com/landaiqing/go-pixelnebula/cache"
"github.com/landaiqing/go-pixelnebula/converter"
"github.com/landaiqing/go-pixelnebula/errors"
"github.com/landaiqing/go-pixelnebula/style"
"github.com/landaiqing/go-pixelnebula/theme"
)
const (
hashLength = 12
keyFactor = 0.47
)
var (
// 优化正则表达式,使用更高效的模式
numberRegex = regexp.MustCompile(`[0-9]`)
// 使用非贪婪模式并优化颜色匹配模式
colorRegex = regexp.MustCompile(`#([^;]*);`)
)
type PNOptions struct {
ThemeIndex int // 主题索引,
StyleIndex int // 风格索引,
}
type PixelNebula struct {
svgEnd string
themeManager *theme.Manager
styleManager *style.Manager
animManager *animation.Manager
cache *cache.PNCache
hasher hash.Hash
options *PNOptions
width int
height int
imgData []byte
}
// NewPixelNebula 创建一个PixelNebula实例
func NewPixelNebula() *PixelNebula {
return &PixelNebula{
svgEnd: "</svg>",
themeManager: theme.NewThemeManager(),
styleManager: style.NewShapeManager(),
animManager: animation.NewAnimationManager(),
hasher: sha256.New(),
options: &PNOptions{ThemeIndex: -1, StyleIndex: -1}, // 初始化为 -1 表示未设置
width: 231,
height: 231,
}
}
// getSvgStart 根据当前宽高生成SVG开始标签
func (pn *PixelNebula) getSvgStart() string {
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %d %d\">", pn.width, pn.height)
}
// WithTheme 设置固定主题
func (pn *PixelNebula) WithTheme(themeIndex int) *PixelNebula {
// 如果已设置 style,则验证主题索引是否有效
if styleIndex := pn.options.StyleIndex; styleIndex >= 0 {
// 获取该风格下的主题数量
themeCount := pn.themeManager.ThemeCount(styleIndex)
if themeIndex < 0 || themeIndex >= themeCount {
log.Printf("pixelnebula: theme index range is:[0, %d), but got %d", themeCount, themeIndex)
panic(errors.ErrInvalidTheme)
}
}
pn.options.ThemeIndex = themeIndex
return pn
}
// WithStyle 设置固定风格
func (pn *PixelNebula) WithStyle(style style.StyleType) *PixelNebula {
styleIndex, err := pn.styleManager.GetStyleIndex(style)
if err != nil {
panic(err)
}
pn.options.StyleIndex = styleIndex
return pn
}
// WithSize 设置尺寸
func (pn *PixelNebula) WithSize(width, height int) *PixelNebula {
pn.width = width
pn.height = height
return pn
}
// WithCustomizeTheme 设置自定义主题
func (pn *PixelNebula) WithCustomizeTheme(theme []theme.Theme) *PixelNebula {
pn.themeManager.CustomizeTheme(theme)
return pn
}
// WithCustomizeStyle 设置自定义风格
func (pn *PixelNebula) WithCustomizeStyle(style []style.StyleSet) *PixelNebula {
pn.styleManager.CustomizeStyle(style)
return pn
}
// hashToNum 将哈希字符串转换为数字
func (pn *PixelNebula) hashToNum(hash []string) int64 {
if len(hash) == 0 {
return 0
}
// 将哈希字符串数组连接成一个字符串
var result int64
for _, h := range hash {
num, err := strconv.ParseInt(h, 10, 64)
if err != nil {
continue
}
// 使用位运算和加法组合多个数字
result = (result << 3) + (result << 1) + num // result * 8 + result * 2 + num
}
// 确保结果为正数
if result < 0 {
result = -result
}
return result
}
// calcKey 计算主题和部分的键值
func (pn *PixelNebula) calcKey(hash []string, opts *PNOptions) [2]int {
// 只有当明确设置了主题和风格索引时才使用固定值
if opts != nil && opts.StyleIndex >= 0 && opts.ThemeIndex >= 0 {
return [2]int{opts.StyleIndex, opts.ThemeIndex}
}
// 直接使用哈希值,不进行 keyFactor 转换
hashNum := pn.hashToNum(hash)
// 获取可用的风格数量
styleCount := pn.themeManager.StyleCount()
if styleCount == 0 {
return [2]int{0, 0}
}
// 使用哈希值计算风格索引
styleIndex := int(hashNum % int64(styleCount))
if styleIndex < 0 {
styleIndex = -styleIndex
}
if styleIndex >= styleCount {
styleIndex = styleCount - 1
}
// 获取该风格下的主题数量
themeCount := pn.themeManager.ThemeCount(styleIndex)
if themeCount == 0 {
return [2]int{styleIndex, 0}
}
// 使用哈希值的不同部分计算主题索引
// 使用更简单的计算方式
themeIndex := int(hashNum % int64(themeCount))
if themeIndex < 0 {
themeIndex = -themeIndex
}
if themeIndex >= themeCount {
themeIndex = themeCount - 1
}
return [2]int{styleIndex, themeIndex}
}
// WithCache 设置缓存选项
func (pn *PixelNebula) WithCache(options cache.CacheOptions) *PixelNebula {
pn.cache = cache.NewCache(options)
return pn
}
// WithDefaultCache 设置默认缓存选项
func (pn *PixelNebula) WithDefaultCache() *PixelNebula {
pn.cache = cache.NewDefaultCache()
return pn
}
// WithCompression 设置压缩选项
func (pn *PixelNebula) WithCompression(options cache.CompressOptions) *PixelNebula {
if pn.cache != nil {
cacheOptions := pn.cache.GetOptions()
cacheOptions.Compression = options
pn.cache.UpdateOptions(cacheOptions)
}
return pn
}
// WithMonitoring 设置监控选项
func (pn *PixelNebula) WithMonitoring(options cache.MonitorOptions) *PixelNebula {
if pn.cache != nil {
cacheOptions := pn.cache.GetOptions()
cacheOptions.Monitoring = options
pn.cache.UpdateOptions(cacheOptions)
// 如果启用了监控但监控器尚未创建,则创建并启动监控器
if options.Enabled && pn.cache.Monitor == nil {
pn.cache.Monitor = cache.NewMonitor(pn.cache, options)
pn.cache.Monitor.Start()
}
}
return pn
}
// WithAnimation 添加动画效果
func (pn *PixelNebula) WithAnimation(animation animation.Animation) *PixelNebula {
pn.animManager.AddAnimation(animation)
return pn
}
// WithRotateAnimation 添加旋转动画
func (pn *PixelNebula) WithRotateAnimation(targetID string, fromAngle, toAngle float64, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewRotateAnimation(targetID, fromAngle, toAngle, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithGradientAnimation 添加渐变动画
func (pn *PixelNebula) WithGradientAnimation(targetID string, colors []string, duration float64, repeatCount int, animate bool) *PixelNebula {
anim := animation.NewGradientAnimation(targetID, colors, duration, repeatCount, animate)
pn.animManager.AddAnimation(anim)
return pn
}
// WithTransformAnimation 添加变换动画
func (pn *PixelNebula) WithTransformAnimation(targetID string, transformType string, from, to string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewTransformAnimation(targetID, transformType, from, to, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithFadeAnimation 添加淡入淡出动画
func (pn *PixelNebula) WithFadeAnimation(targetID string, from, to string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewFadeAnimation(targetID, from, to, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithPathAnimation 添加路径动画
func (pn *PixelNebula) WithPathAnimation(targetID string, path string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewPathAnimation(targetID, path, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithPathAnimationRotate 添加带旋转的路径动画
func (pn *PixelNebula) WithPathAnimationRotate(targetID string, path string, rotate string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewPathAnimation(targetID, path, duration, repeatCount)
anim.WithRotate(rotate)
pn.animManager.AddAnimation(anim)
return pn
}
// WithColorAnimation 添加颜色变换动画
func (pn *PixelNebula) WithColorAnimation(targetID string, property string, fromColor, toColor string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewColorAnimation(targetID, property, fromColor, toColor, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithBounceAnimation 添加弹跳动画
func (pn *PixelNebula) WithBounceAnimation(targetID string, property string, from, to string, bounceCount int, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewBounceAnimation(targetID, property, from, to, bounceCount, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithWaveAnimation 添加波浪动画
func (pn *PixelNebula) WithWaveAnimation(targetID string, amplitude, frequency float64, direction string, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewWaveAnimation(targetID, amplitude, frequency, direction, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// WithBlinkAnimation 添加闪烁动画
func (pn *PixelNebula) WithBlinkAnimation(targetID string, minOpacity, maxOpacity float64, blinkCount int, duration float64, repeatCount int) *PixelNebula {
anim := animation.NewBlinkAnimation(targetID, minOpacity, maxOpacity, blinkCount, duration, repeatCount)
pn.animManager.AddAnimation(anim)
return pn
}
// SVGBuilder 用于处理SVG生成后的链式操作
type SVGBuilder struct {
pn *PixelNebula
svg string
id string
sansEnv bool
themeIndex int
styleIndex int
width int
height int
hasError error
}
// Generate 现在返回 SVGBuilder
func (pn *PixelNebula) Generate(id string, sansEnv bool) *SVGBuilder {
return &SVGBuilder{
pn: pn,
id: id,
sansEnv: sansEnv,
width: pn.width,
height: pn.height,
themeIndex: pn.options.ThemeIndex,
styleIndex: pn.options.StyleIndex,
}
}
// SetTheme 设置主题
func (sb *SVGBuilder) SetTheme(theme int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
themeCount := sb.pn.themeManager.ThemeCount(sb.styleIndex)
if theme < 0 || theme >= themeCount {
log.Printf("pixelnebula: theme index range is:[0, %d), but got %d", themeCount, theme)
sb.hasError = errors.ErrInvalidTheme
return sb
}
sb.themeIndex = theme
return sb
}
// SetStyle 设置风格
// 注意当使用WithCustomizeStyle设置自定义风格后此方法将无法正常工作应使用SetStyleByIndex代替
func (sb *SVGBuilder) SetStyle(style style.StyleType) *SVGBuilder {
if sb.hasError != nil {
return sb
}
index, err := sb.pn.styleManager.GetStyleIndex(style)
if err != nil {
sb.hasError = err
return sb
}
sb.styleIndex = index
return sb
}
// SetStyleByIndex 设置风格索引
// 此方法可用于设置自定义风格的索引特别是在使用WithCustomizeStyle后
func (sb *SVGBuilder) SetStyleByIndex(index int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
themeCount := sb.pn.themeManager.StyleCount()
if index < 0 || index >= themeCount {
log.Printf("pixelnebula: style index range is:[0, %d), but got %d", themeCount, index)
sb.hasError = errors.ErrInvalidStyleName
return sb
}
sb.styleIndex = index
return sb
}
// SetSize 设置尺寸
func (sb *SVGBuilder) SetSize(width, height int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
sb.width = width
sb.height = height
return sb
}
// SetAnimation 添加动画效果
func (sb *SVGBuilder) SetAnimation(anim animation.Animation) *SVGBuilder {
if sb.hasError != nil {
return sb
}
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetRotateAnimation 添加旋转动画
func (sb *SVGBuilder) SetRotateAnimation(targetID string, fromAngle, toAngle float64, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewRotateAnimation(targetID, fromAngle, toAngle, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetGradientAnimation 添加渐变动画
func (sb *SVGBuilder) SetGradientAnimation(targetID string, colors []string, duration float64, repeatCount int, animate bool) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewGradientAnimation(targetID, colors, duration, repeatCount, animate)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetTransformAnimation 添加变换动画
func (sb *SVGBuilder) SetTransformAnimation(targetID string, transformType string, from, to string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewTransformAnimation(targetID, transformType, from, to, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetFadeAnimation 添加淡入淡出动画
func (sb *SVGBuilder) SetFadeAnimation(targetID string, from, to string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewFadeAnimation(targetID, from, to, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetPathAnimation 添加路径动画
func (sb *SVGBuilder) SetPathAnimation(targetID string, path string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewPathAnimation(targetID, path, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetPathAnimationRotate 添加带旋转的路径动画
func (sb *SVGBuilder) SetPathAnimationRotate(targetID string, path string, rotate string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewPathAnimation(targetID, path, duration, repeatCount)
anim.WithRotate(rotate)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetColorAnimation 添加颜色变换动画
func (sb *SVGBuilder) SetColorAnimation(targetID string, property string, fromColor, toColor string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewColorAnimation(targetID, property, fromColor, toColor, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetBounceAnimation 添加弹跳动画
func (sb *SVGBuilder) SetBounceAnimation(targetID string, property string, from, to string, bounceCount int, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewBounceAnimation(targetID, property, from, to, bounceCount, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetWaveAnimation 添加波浪动画
func (sb *SVGBuilder) SetWaveAnimation(targetID string, amplitude, frequency float64, direction string, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewWaveAnimation(targetID, amplitude, frequency, direction, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// SetBlinkAnimation 添加闪烁动画
func (sb *SVGBuilder) SetBlinkAnimation(targetID string, minOpacity, maxOpacity float64, blinkCount int, duration float64, repeatCount int) *SVGBuilder {
if sb.hasError != nil {
return sb
}
anim := animation.NewBlinkAnimation(targetID, minOpacity, maxOpacity, blinkCount, duration, repeatCount)
sb.pn.animManager.AddAnimation(anim)
return sb
}
// Build 生成最终的SVG
func (sb *SVGBuilder) Build() *SVGBuilder {
if sb.hasError != nil {
return sb
}
opts := &PNOptions{
ThemeIndex: sb.themeIndex,
StyleIndex: sb.styleIndex,
}
svg, err := sb.pn.generateSVG(sb.id, sb.sansEnv, opts)
if err != nil {
sb.hasError = err
return sb
}
sb.svg = svg
sb.pn.imgData = []byte(svg)
sb.pn.width = sb.width
sb.pn.height = sb.height
return sb
}
// ToSVG 获取SVG字符串
func (sb *SVGBuilder) ToSVG() (string, error) {
if sb.svg == "" {
sb = sb.Build()
}
if sb.hasError != nil {
return "", sb.hasError
}
return sb.svg, nil
}
// ToBase64 获取Base64编码的SVG字符串 注意:这个设置宽高无效
func (sb *SVGBuilder) ToBase64() (string, error) {
if sb.svg == "" {
sb = sb.Build()
}
if sb.hasError != nil {
return "", sb.hasError
}
conv := converter.NewSVGConverter([]byte(sb.svg), sb.width, sb.height)
return conv.ToBase64()
}
// ToFile 将SVG代码保存到文件
func (sb *SVGBuilder) ToFile(filePath string) error {
if sb.svg == "" {
sb = sb.Build()
}
if sb.hasError != nil {
return sb.hasError
}
return os.WriteFile(filePath, []byte(sb.svg), 0644)
}
// 将原来的 GenerateSVG 重命名为 generateSVG作为内部方法
func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (svg string, err error) {
if opts == nil {
opts = pn.options
}
// 验证参数
if id == "" {
return "", errors.ErrAvatarIDRequired
}
// 如果启用了缓存,先尝试从缓存获取
if pn.cache != nil {
cacheKey := cache.CacheKey{
Id: id,
SansEnv: sansEnv,
}
if opts != nil {
cacheKey.Theme = opts.ThemeIndex
cacheKey.Part = opts.StyleIndex
}
if cachedSVG, found := pn.cache.Get(cacheKey); found {
return cachedSVG, nil
}
}
// 计算avatarId的哈希值
pn.hasher.Reset()
pn.hasher.Write([]byte(id))
sum := pn.hasher.Sum(nil)
s := hex.EncodeToString(sum)
hashStr := numberRegex.FindAllString(s, -1)
if len(hashStr) < hashLength {
return "", errors.ErrInsufficientHash
}
hashStr = hashStr[0:hashLength]
// 预分配map容量以提高性能
var p = make(map[string][2]int, 6)
p[string(style.TypeEnv)] = pn.calcKey(hashStr[:2], opts)
p[string(style.TypeClo)] = pn.calcKey(hashStr[2:4], opts)
p[string(style.TypeHead)] = pn.calcKey(hashStr[4:6], opts)
p[string(style.TypeMouth)] = pn.calcKey(hashStr[6:8], opts)
p[string(style.TypeEyes)] = pn.calcKey(hashStr[8:10], opts)
p[string(style.TypeTop)] = pn.calcKey(hashStr[10:], opts)
// 预分配map容量
var final = make(map[string]string, 6)
for k, v := range p {
// 获取主题颜色
themePart, err := pn.themeManager.GetTheme(v[0], v[1])
if err != nil {
return "", err
}
colors, ok := themePart[k]
if !ok {
return "", errors.ErrInvalidColor
}
// 获取形状SVG
shapeType := style.ShapeType(k)
svgPart, err := pn.styleManager.GetShape(v[0], shapeType)
if err != nil {
return "", err
}
match := colorRegex.FindAllStringSubmatch(svgPart, -1)
// 使用strings.Builder提高字符串处理性能
var sb strings.Builder
sb.Grow(len(svgPart) + 50) // 预分配足够的容量
lastIndex := 0
for i, m := range match {
if i < len(colors) {
// 找到完整匹配的位置
index := strings.Index(svgPart[lastIndex:], m[0]) + lastIndex
// 添加匹配前的部分
sb.WriteString(svgPart[lastIndex:index])
// 添加替换后的颜色
// 检查颜色值是否已经包含#前缀
if strings.HasPrefix(colors[i], "#") {
sb.WriteString(colors[i])
} else {
sb.WriteString("#")
sb.WriteString(colors[i])
}
sb.WriteString(";")
// 更新lastIndex
lastIndex = index + len(m[0])
}
}
// 添加剩余部分
sb.WriteString(svgPart[lastIndex:])
final[k] = sb.String()
}
// 使用strings.Builder构建最终SVG
var builder strings.Builder
// 预估SVG大小避免多次内存分配
builder.Grow(1024 * 2)
builder.WriteString(pn.getSvgStart()) // 使用动态生成的svgStart
// 获取动画定义
animations := pn.animManager.GenerateSVGAnimations()
if animations != "" {
builder.WriteString(animations)
}
// 检查是否有旋转动画并获取旋转动画的SVG代码
rotateAnimations := make(map[string]bool)
rotateAnimationSVGs := make(map[string]string)
for _, anim := range pn.animManager.GetAnimations() {
// 检查是否为旋转动画
if rotateAnim, ok := anim.(*animation.RotateAnimation); ok {
rotateAnimations[anim.GetTargetID()] = true
// 获取旋转动画的SVG代码只提取animateTransform部分
svgCode := rotateAnim.GenerateSVG()
// 提取animateTransform标签
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
rotateAnimationSVGs[anim.GetTargetID()] = svgCode[start : start+end+2]
}
}
}
}
// 处理元素如果元素有旋转动画则包裹在g标签中并添加animateTransform
// 只有当不是无环境模式时才添加环境
if !sansEnv {
if _, hasRotate := rotateAnimations["env"]; hasRotate {
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
builder.WriteString(final["env"])
// 添加animateTransform标签
if animSVG, ok := rotateAnimationSVGs["env"]; ok {
builder.WriteString(animSVG)
}
builder.WriteString("</g>\n")
} else {
builder.WriteString(final["env"])
}
}
// 处理其他元素
elements := []string{"head", "clo", "top", "eyes", "mouth"}
// 单独处理每个元素
for _, elem := range elements {
if _, hasRotate := rotateAnimations[elem]; hasRotate {
// 如果元素有旋转动画则包裹在g标签中
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
builder.WriteString(final[elem])
// 添加animateTransform标签
if animSVG, ok := rotateAnimationSVGs[elem]; ok {
// 提取animateTransform标签部分
if start := strings.Index(animSVG, "<animateTransform"); start != -1 {
if end := strings.Index(animSVG[start:], "/>"); end != -1 {
builder.WriteString(animSVG[start : start+end+2])
}
}
}
builder.WriteString("</g>\n")
} else {
// 如果元素没有旋转动画,直接添加
builder.WriteString(final[elem])
}
}
builder.WriteString(pn.svgEnd)
svg = builder.String()
pn.imgData = []byte(svg)
// 如果启用了缓存,将结果存入缓存
if pn.cache != nil {
cacheKey := cache.CacheKey{
Id: id,
SansEnv: sansEnv,
}
if opts != nil {
cacheKey.Theme = opts.ThemeIndex
cacheKey.Part = opts.StyleIndex
}
pn.cache.Set(cacheKey, svg)
}
return svg, nil
}