Files
go-pixelnebula/pixelnebula.go
2025-03-19 21:10:19 +08:00

742 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}