commit 86c8755f7930598da3484e18f5a6d0b1a19262bb Author: landaiqing Date: Wed Mar 19 21:10:19 2025 +0800 :tada: Initial commit diff --git a/ .gitignore b/ .gitignore new file mode 100644 index 0000000..ffbf8f9 --- /dev/null +++ b/ .gitignore @@ -0,0 +1,109 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# temporary files, cache and logs +tmp/ + +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0f0c3aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 landaiqing + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..633d0a1 --- /dev/null +++ b/README.md @@ -0,0 +1,621 @@ +
+ +#
PixelNebula
+ +
+

中文 | English

+
+ +

+ PixelNebula Logo +

+ +
+

+ 🚀 A powerful, efficient, and customizable SVG avatar generation library for Go +

+
+ +
+ Go Reference + Go Report Card + License + Version + PRs Welcome +
+ +

+ ✨ Features • + 📦 Installation • + 🚀 Quick Start • + 🔧 Advanced Usage • + 💫 Animation Effects • + ⚡ Cache System • + 📊 Benchmarks • + 💡 Examples and Demos • + 👥 Contribution Guide • + 📄 License +

+ +
+ +
+ +## 📋 Introduction + + + + + + +
+

PixelNebula is a high-performance SVG avatar generation library written in Go, focused on creating beautiful, customizable, and highly animated vector avatars. With PixelNebula, you can easily generate avatars in various styles, add animation effects, and apply different themes and style variations.

+

Whether you're creating user avatars for applications, generating unique identifier icons, or making dynamic visual effects, PixelNebula can meet your needs.

+
+

+ PixelNebula Demo +
+ Sample Avatar Display
Sample Code +

+
+ +
+ +## ✨ Features + +
+ + + + + + + + +
+ Various Styles
+ Various Styles +
+ Animation Effects
+ Animation Effects +
+ Customizable
+ Customizable +
+ High Performance
+ High Performance +
+ Cache System
+ Cache System +
+
+ +- 🎨 **Various Styles and Themes**: Built-in multiple styles and theme combinations to meet different design needs +- 🔄 **Rich Animation Effects**: Support for rotation, gradient, transformation, fade-in/out, and other animations +- 🛠️ **Fully Customizable**: Custom styles, themes, colors, and animation effects +- ⚡ **High-Performance Design**: Optimized code structure and caching mechanism for fast generation +- 💾 **Smart Cache System**: Built-in cache system to improve efficiency of repetitive generation +- 📊 **Cache Monitoring**: Support for cache usage monitoring and analysis +- 🔍 **Chainable API**: Clean and clear API design with support for fluent chaining +- 📱 **Responsive Design**: Support for custom sizes, adapting to various display environments + +
+ +## 📦 Installation + +Using Go toolchain to install the package: + +```bash +go get github.com/landaiqing/go-pixelnebula +``` + +
+ +## 🚀 Quick Start + +### Basic Usage + +Below is a basic example of generating a simple SVG avatar: + + + + + + +
+ +```go +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +func main() { + // Create a new PixelNebula instance + pn := pixelnebula.NewPixelNebula() + + // Set style and size + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // Generate SVG and save to file + svg, err := pn.Generate("unique-id-123", false).ToSVG() + if err != nil { + fmt.Printf("Failed to generate SVG: %v\n", err) + return + } + + // Save to file + err = os.WriteFile("my_avatar.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("Failed to save file: %v\n", err) + return + } + + fmt.Println("Avatar successfully generated: my_avatar.svg") +} +``` + + + +
+ +
+ +## 🔧 Advanced Usage + +### Custom Themes and Styles + +
+Click to Expand/Collapse Code Example + +```go +// Custom style +customStyles := []style.StyleSet{ + { + // First custom style + style.TypeEnv: ``, + style.TypeHead: ``, + style.TypeClo: ``, + style.TypeEyes: ``, + style.TypeMouth: ``, + style.TypeTop: ``, + }, +} + +// Apply custom style +pn2.WithCustomizeStyle(customStyles) +// Custom theme +customThemes := []theme.Theme{ +{ + theme.ThemePart{ + // Environment part colors + "env": []string{"#FF5733", "#C70039"}, + // Head colors + "head": []string{"#FFC300", "#FF5733"}, + // Clothes colors + "clo": []string{"#2E86C1", "#1A5276"}, + // Eyes colors + "eyes": []string{"#000000", "#FFFFFF"}, + // Mouth colors + "mouth": []string{"#E74C3C"}, + // Top decoration colors + "top": []string{"#884EA0", "#7D3C98"}, + }, + }, +} + +pn.WithCustomizeTheme(customTheme) +``` + +
+ +### Using SVGBuilder Chainable API + +
+Click to Expand/Collapse Code Example + +```go +pn := NewPixelNebula().WithDefaultCache() +pn.Generate("my-avatar", false). + SetStyle(style.GirlStyle). + SetTheme(0). + SetSize(231, 231). + SetRotateAnimation("env", 0, 360, 10, -1). + SetGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true). + Build(). + ToSVG() +``` + +
+ +
+ +## 💫 Animation Effects + +PixelNebula supports multiple animation effects to bring your avatars to life: + +
+ + + + + + + +
+ Rotation Animation
+ Rotation +
+ Gradient Animation
+ Gradient +
+ Fade In/Out
+ Fade In/Out +
+ Path Animation
+ Path +
+
+ +
+Click to View Animation Code Examples + +```go +pn := NewPixelNebula() + +// Set style +pn.WithStyle(style.AfrohairStyle) +pn.WithTheme(0) + +// 1. Rotation animation - Rotate environment and head +pn.WithRotateAnimation("env", 0, 360, 10, -1) // Infinite loop environment rotation + +// 2. Gradient animation - Environment gradient +pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) +// 2. Gradient animation - Eyes gradient +pn.WithGradientAnimation("eyes", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + +// 3. Fade in/out animation - Eyes blinking +pn.WithFadeAnimation("eyes", "1", "0.3", 2, -1) + +// 4. Transform animation - Mouth scaling +//pn.WithTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1) + +// 5. Color animation - Hair color change +pn.WithColorAnimation("top", "fill", "#9b59b6", "#e74c3c", 3, -1) +// 5. Color animation - Clothes color change +pn.WithColorAnimation("clo", "fill", "#9b59b6", "#e74c3c", 3, -1) + +// 6. Bounce animation - Mouth bouncing +pn.WithBounceAnimation("mouth", "transform", "0,0", "0,-10", 5, 2.5, -1) +// 6. Rotation animation - Mouth rotation +pn.WithRotateAnimation("mouth", 0, 360, 10, -1) // Infinite loop mouth rotation + +//// 7. Wave animation - Clothes wave effect +//pn.WithWaveAnimation("clo", 5, 0.2, "horizontal", 4, -1) + +// 8. Blink animation - Top decoration blinking +//pn.WithBlinkAnimation("head", 0.3, 1.0, 4, 6, -1) +// 8. Wave animation - Environment wave effect +//pn.WithWaveAnimation("clo", 5, 2, "horizontal", 4, -1) + +// 9. Path animation - Eyes moving along path +//pn.WithPathAnimation("eyes", "M 0,0 C 10,-10 -10,-10 0,0", 3, -1) + +pn.WithBounceAnimation("eyes", "transform", "0,0", "0,-5", 5, 2, -1) + +// 10. Path animation with rotation - Eyes rotating while moving +//pn.WithPathAnimationRotate("eyes", "M 0,0 C 5,5 -5,5 0,0", "auto", 4, -1) +``` + +
+ +
+ +## ⚡ Cache System + +PixelNebula has a built-in smart cache system to improve the efficiency of repeated generation: + +
+Click to View Cache Configuration Code Examples + +```go +// Use default cache configuration +pn.WithDefaultCache() + +// Custom cache configuration +customCacheOptions := cache.CacheOptions{ + Enabled: true, + DirectorySize: 100, // Maximum cache entries + Expiration: 1 * time.Hour, // Cache expiration time +//... Other configuration options +} +// Create a PixelNebula instance with custom cache +pn := pixelnebula.NewPixelNebula().WithCache(customCacheOptions) + +// Enable cache monitoring +pn.WithMonitoring(cache.MonitorOptions{ + Enabled: true, + SampleInterval: 5 * time.Second, +//... Other configuration options +}) + +// Enable cache compression +pn.WithCompression(cache.CompressOptions{ + Enabled: true, + Level: 6, + MinSizeBytes: 100, // Minimum compression size (bytes) + //... Other configuration options +}) +``` + +
+ +
+ +## 📊 Benchmarks + +We have conducted comprehensive benchmark tests on PixelNebula to ensure high performance in various scenarios. Below are sample test results (Test environment: Intel i7-12700K, 32GB RAM): + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationTime per OperationMemory AllocationAllocations
Basic Avatar Generation3.5 ms/op328 KB/op52 allocs/op
No-Environment Avatar2.8 ms/op256 KB/op48 allocs/op
With Rotation Animation4.2 ms/op384 KB/op62 allocs/op
Using Cache (Hit)0.3 ms/op48 KB/op12 allocs/op
Concurrent Generation (10)5.7 ms/op392 KB/op58 allocs/op
+
+ +### Running Benchmarks + +To run benchmarks in your own environment, execute: + +```bash +cd benchmark +go test -bench=. -benchmem +``` + +For more detailed benchmark information, see [benchmark/README.md](benchmark/README.md). + +
+ +## 💡 Examples and Demos + +### 📚 Example Code + +We've prepared a complete set of example code covering all core features to help you get started quickly. + +
+ + + + + + + + + + + + + + + + +
+ Basic Usage
+ Basic Usage
+ View Code +
+ Styles & Themes
+ Styles & Themes
+ View Code +
+ Custom Theme
+ Custom Theme
+ View Code +
+ Animations
+ Animations
+ View Code +
+ Chain API
+ Chain API
+ View Code +
+ Cache System
+ Cache System
+ View Code +
+ Format Conversion
+ Format Conversion
+ View Code +
+ Random Avatar Generator
+ Random Avatar Generator
+ View Code +
+

...

+
+
+ +
+📋 Detailed Example Descriptions + +1. **Basic Usage** [01_basic_usage.go](examples/01_basic_usage.go) + - Create basic PixelNebula avatars + - Generate regular avatars and no-environment avatars + - Demonstrate basic configuration and error handling + +2. **Styles and Themes** [02_styles_and_themes.go](examples/02_styles_and_themes.go) + - Use different styles to generate avatars + - Apply multiple theme combinations + - Demonstrate cycling through styles and themes + +3. **Custom Themes and Styles** [03_custom_theme_and_style.go](examples/03_custom_theme_and_style.go) + - Create custom themes + - Define custom styles + - Demonstrate combining themes and styles + +4. **Animation Effects** [04_all_animations.go](examples/04_all_animations.go) + - Rotation animation + - Gradient animation + - Fade-in/out effects + - Transform animation + - Color transformation + - Bounce effects + - Wave animation + - Blink effects + - Path animation + +5. **SVG Builder Chainable API** [05_svg_builder_chain.go](examples/05_svg_builder_chain.go) + - Basic chainable calls + - Chainable calls with animation + - Direct save to file + - Base64 conversion + +6. **Cache System** [06_cache_system.go](examples/06_cache_system.go) + - Using default cache + - Custom cache configuration + - Cache monitoring functionality + - Compressed cache examples + +7. **Format Conversion** [07_format_conversion.go](examples/07_format_conversion.go) + - Base64 encoding + - We haven't found a perfect solution for other formats yet, please feel free to make a PR + +8. **Random Avatar Generator** [08_random_avatar_generator.go](examples/08_random_avatar_generator.go) + - Generate random avatars with different styles and themes + +
+ +### 🎮 Running Examples + +
+📋 How to Run Examples + +1. **Prerequisites** + - Ensure Go is installed (Go 1.16+ recommended) + - Make sure GOPATH is properly set + +2. **Clone the Repository** + ```bash + git clone github.com/landaiqing/go-pixelnebula.git + cd go-pixelnebula + ``` + +3. **Run a Single Example** + ```bash + go run examples/01_basic_usage.go + ``` + +4. **Run All Examples** + ```bash + for file in examples/*_*.go; do + echo "🚀 Running: $file" + go run $file + echo "------------" + done + ``` + +5. **Run Benchmarks** + ```bash + cd benchmark + go test -bench=. -benchmem + ``` + +**💡 Tip:** Check the top comments in each example for detailed functionality and customization options. + +
+ +
+ +## 👥 Contribution Guide + +Contributions of code, issue reports, or suggestions are welcome! Please follow these steps: + +
+ + + + + + + + +
+ Step 1
+ Fork the Repository +
+ Step 2
+ Create a Branch
+ git checkout -b feature/amazing-feature +
+ Step 3
+ Commit Changes
+ git commit -m 'Add some feature' +
+ Step 4
+ Push Branch
+ git push origin feature/amazing-feature +
+ Step 5
+ Open PR +
+
+ +
+ +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +
+ +
+

Made with ❤️ and Go

+

© 2025 landaiqing

+ +
+
\ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..d77412c --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,631 @@ +
+ +#
PixelNebula
+ +
+

中文 | English

+
+ +

+ PixelNebula Logo +

+ +
+

+ 🚀 一个强大、高效且可自定义的 Go 语言 SVG 动态头像生成库 +

+
+ +
+ Go Reference + Go Report Card + License + Version + PRs Welcome +
+ +

+ ✨ 特性 • + 📦 安装 • + 🚀 快速开始 • + 🔧 高级用法 • + 💫 动画效果 • + ⚡ 缓存系统 • + 📊 基准测试 • + 💡 示例和演示 • + 👥 贡献指南 • + 📄 许可证 +

+ +
+ +
+ +## 📋 介绍 + + + + + + +
+

PixelNebula 是一个 Go 语言编写的高性能 SVG 头像生成库,专注于创建精美、可定制且高度可动态化的矢量头像。使用 PixelNebula,您可以轻松生成各种风格的头像,添加动画效果,并应用各种主题和样式变化。

+

无论是为应用程序创建用户头像、生成唯一标识图标,还是制作动态可视化效果,PixelNebula 都能满足您的需求。

+
+

+ PixelNebula Demo +
+ 示例头像展示
示例代码 +

+
+ +
+ +## ✨ 特性 + +
+ + + + + + + + +
+ 多样化风格
+ 多样化风格 +
+ 动画效果
+ 动画效果 +
+ 可自定义
+ 可自定义 +
+ 高性能
+ 高性能 +
+ 缓存系统
+ 缓存系统 +
+
+ +- 🎨 **多样化风格与主题**: 内置多种风格和主题组合,满足不同设计需求 +- 🔄 **丰富动画效果**: 支持旋转、渐变、变换、淡入淡出等多种动画 +- 🛠️ **完全可自定义**: 自定义风格、主题、颜色和动画效果 +- ⚡ **高性能设计**: 优化的代码结构和缓存机制,确保快速生成 +- 💾 **智能缓存系统**: 内置缓存系统,提高重复生成效率 +- 📊 **缓存监控**: 支持缓存使用情况监控和分析 +- 🔍 **链式调用 API**: 简洁明了的 API 设计,支持流畅的链式调用 +- 📱 **响应式设计**: 支持自定义尺寸,适应各种显示环境 + +
+ +## 📦 安装 + +使用 Go 工具链安装包: + +```bash +go get github.com/landaiqing/go-pixelnebula +``` + +
+ +## 🚀 快速开始 + +### 基本用法 + +以下是生成简单 SVG 头像的基本示例: + + + + + + +
+ +```go +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +func main() { + // 创建一个新的 PixelNebula 实例 + pn := pixelnebula.NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // 生成 SVG 并保存到文件 + svg, err := pn.Generate("unique-id-123", false).ToSVG() + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + return + } + + // 保存到文件 + err = os.WriteFile("my_avatar.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + return + } + + fmt.Println("头像成功生成: my_avatar.svg") +} +``` + + + +
+ +
+ +## 🔧 高级用法 + +### 自定义主题和风格 + +
+点击展开/折叠代码示例 + +```go +// 自定义风格 +customStyles := []style.StyleSet{ + { + // 第一种自定义风格 + style.TypeEnv: ``, + style.TypeHead: ``, + style.TypeClo: ``, + style.TypeEyes: ``, + style.TypeMouth: ``, + style.TypeTop: ``, + }, +} + +// 应用自定义风格 +pn2.WithCustomizeStyle(customStyles) +// 自定义主题 +customThemes := []theme.Theme{ +{ + theme.ThemePart{ + // 环境部分颜色 + "env": []string{"#FF5733", "#C70039"}, + // 头部颜色 + "head": []string{"#FFC300", "#FF5733"}, + // 衣服颜色 + "clo": []string{"#2E86C1", "#1A5276"}, + // 眼睛颜色 + "eyes": []string{"#000000", "#FFFFFF"}, + // 嘴巴颜色 + "mouth": []string{"#E74C3C"}, + // 头顶装饰颜色 + "top": []string{"#884EA0", "#7D3C98"}, + }, + }, +} + +pn.WithCustomizeTheme(customTheme) + +``` + +
+ +### 使用 SVGBuilder 链式调用 + +
+点击展开/折叠代码示例 + +```go +pn := NewPixelNebula().WithDefaultCache() +pn.Generate("my-avatar", false). + SetStyle(style.GirlStyle). + SetTheme(0). + SetSize(231, 231). + SetRotateAnimation("env", 0, 360, 10, -1). + SetGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true). + Build(). + ToSVG() +``` + +
+ +
+ +## 💫 动画效果 + +PixelNebula 支持多种动画效果,可以让您的头像栩栩如生: + +
+ + + + + + + + +
+ 旋转动画
+ 旋转动画 +
+ 渐变动画
+ 渐变动画 +
+ 淡入淡出
+ 淡入淡出 +
+ 路径动画
+ 路径动画 +
+

...

+
+
+ +
+点击查看动画代码示例 + +```go +pn := NewPixelNebula() + +// 设置风格 +pn.WithStyle(style.AfrohairStyle) +pn.WithTheme(0) + +// 1. 旋转动画 - 让环境和头部旋转 +pn.WithRotateAnimation("env", 0, 360, 10, -1) // 无限循环旋转环境 + +// 2. 渐变动画 - 让环境渐变 +pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) +// 2. 渐变动画 - 让眼睛渐变 +pn.WithGradientAnimation("eyes", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + +// 3. 淡入淡出动画 - 让眼睛闪烁 +pn.WithFadeAnimation("eyes", "1", "0.3", 2, -1) + +// 4. 变换动画 - 让嘴巴缩放 +//pn.WithTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1) + +// 5. 颜色变换动画 - 让头发颜色变换 +pn.WithColorAnimation("top", "fill", "#9b59b6", "#e74c3c", 3, -1) +// 5. 颜色变换动画 - 让衣服颜色变换 +pn.WithColorAnimation("clo", "fill", "#9b59b6", "#e74c3c", 3, -1) + +// 6. 弹跳动画 - 让嘴巴弹跳 +pn.WithBounceAnimation("mouth", "transform", "0,0", "0,-10", 5, 2.5, -1) +// 6. 旋转动画 - 让嘴巴旋转 +pn.WithRotateAnimation("mouth", 0, 360, 10, -1) // 无限循环旋转环境 + +//// 7. 波浪动画 - 让衣服产生波浪效果 +//pn.WithWaveAnimation("clo", 5, 0.2, "horizontal", 4, -1) + +// 8. 闪烁动画 - 让头顶装饰闪烁 +//pn.WithBlinkAnimation("head", 0.3, 1.0, 4, 6, -1) +// 8. 波浪动画 - 让环境产生波浪效果 +//pn.WithWaveAnimation("clo", 5, 2, "horizontal", 4, -1) + +// 9. 路径动画 - 让眼睛沿着路径移动 +//pn.WithPathAnimation("eyes", "M 0,0 C 10,-10 -10,-10 0,0", 3, -1) + +pn.WithBounceAnimation("eyes", "transform", "0,0", "0,-5", 5, 2, -1) + +// 10. 带旋转的路径动画 - 让眼睛在移动的同时旋转 +//pn.WithPathAnimationRotate("eyes", "M 0,0 C 5,5 -5,5 0,0", "auto", 4, -1) + +``` + +
+ +
+ +## ⚡ 缓存系统 + +PixelNebula 内置智能缓存系统,提高重复生成的效率。 + +
+点击查看缓存配置代码示例 + +```go +// 使用默认缓存配置 +pn.WithDefaultCache() + +// 自定义缓存配置 +customCacheOptions := cache.CacheOptions{ + Enabled: true, + DirectorySize: 100, // 最大缓存条目数 + Expiration: 1 * time.Hour, // 缓存有效期 +//... 其他配置项 +} +// 创建一个带自定义缓存的PixelNebula实例 +pn := pixelnebula.NewPixelNebula().WithCache(customCacheOptions) + +// 启用缓存监控 +pn.WithMonitoring(cache.MonitorOptions{ + Enabled: true, + SampleInterval: 5 * time.Second, +//... 其他配置项 +}) + +// 启用缓存压缩 +pn.WithCompression(cache.CompressOptions{ + Enabled: true, + Level: 6, + MinSizeBytes: 100, // 最小压缩大小 (字节) + //... 其他配置项 +}) +``` + +
+ +
+ +## 📊 基准测试 + +我们对 PixelNebula 进行了全面的基准测试,确保在各种场景下都能保持高性能表现。下面是测试结果示例(测试环境:Intel i7-12700K, +32GB RAM): + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作每次操作耗时内存分配分配次数
基本头像生成3.5 ms/op328 KB/op52 allocs/op
无环境头像2.8 ms/op256 KB/op48 allocs/op
添加旋转动画4.2 ms/op384 KB/op62 allocs/op
使用缓存(命中)0.3 ms/op48 KB/op12 allocs/op
并发生成(10)5.7 ms/op392 KB/op58 allocs/op
+
+ +### 运行基准测试 + +想要在自己的环境中运行基准测试,请执行: + +```bash +cd benchmark +go test -bench=. -benchmem +``` + +更多详细的基准测试信息,请查看 [benchmark/README.md](benchmark/README.md)。 + +
+ +## 💡 示例和演示 + +### 📚 示例代码 + +我们准备了一套完整的示例代码,涵盖了 PixelNebula 的所有核心功能,帮助您快速上手使用。 + +
+ + + + + + + + + + + + + + + + + +
+ 基础用法
+ 基础用法
+ 查看代码 +
+ 样式和主题
+ 样式和主题
+ 查看代码 +
+ 自定义主题
+ 自定义主题
+ 查看代码 +
+ 动画效果
+ 动画效果
+ 查看代码 +
+ 链式API
+ 链式API
+ 查看代码 +
+ 缓存系统
+ 缓存系统
+ 查看代码 +
+ 格式转换
+ 格式转换
+ 查看代码 +
+ 随机头像生成器
+ 随机头像生成器
+ 查看代码 +
+

...

+
+
+ +
+📋 示例详细说明 + +1. **基础用法** [01_basic_usage.go](examples/01_basic_usage.go) + - 创建基本的 PixelNebula 头像 + - 生成常规头像和无环境头像 + - 演示基本配置和错误处理 + +2. **样式和主题** [02_styles_and_themes.go](examples/02_styles_and_themes.go) + - 使用不同样式生成头像 + - 应用多种主题组合 + - 演示循环使用样式和主题 + +3. **自定义主题和样式** [03_custom_theme_and_style.go](examples/03_custom_theme_and_style.go) + - 创建自定义主题 + - 定义自定义样式 + - 演示组合主题和样式 + +4. **动画效果** [04_all_animations.go](examples/04_all_animations.go) + - 旋转动画 + - 渐变动画 + - 淡入淡出效果 + - 变换动画 + - 颜色变换 + - 弹跳效果 + - 波浪动画 + - 闪烁效果 + - 路径动画 + +5. **SVG构建器链式API** [05_svg_builder_chain.go](examples/05_svg_builder_chain.go) + - 基本链式调用 + - 带动画的链式调用 + - 直接保存到文件 + - Base64转换 + +6. **缓存系统** [06_cache_system.go](examples/06_cache_system.go) + - 使用默认缓存 + - 自定义缓存配置 + - 缓存监控功能 + - 压缩缓存示例 + +7. **格式转换** [07_format_conversion.go](examples/07_format_conversion.go) + - Base64编码 + - 其他格式暂未找到完美解决方案,欢迎 PR + +8. **随机头像生成器** [08_random_avatar_generator.go](examples/08_random_avatar_generator.go) + - 随机生成不同风格和主题的头像 + +
+ +### 🎮 运行示例 + +
+📋 如何运行示例 + +1. **运行准备** + - 确保已安装 Go 环境(建议 Go 1.16+) + - 正确设置 GOPATH + +2. **克隆代码库** + ```bash + git clone github.com/landaiqing/go-pixelnebula.git + cd go-pixelnebula + ``` + +3. **运行单个示例** + ```bash + go run examples/01_basic_usage.go + ``` + +4. **运行所有示例** + ```bash + for file in examples/*_*.go; do + echo "🚀 运行: $file" + go run $file + echo "------------" + done + ``` + +5. **运行基准测试** + ```bash + cd benchmark + go test -bench=. -benchmem + ``` + +**💡 提示:** 查看每个示例的顶部注释,了解功能详情和可自定义的部分。 + +
+ +
+ +## 👥 贡献指南 + +欢迎贡献代码、报告问题或提出建议!请遵循以下步骤: + +
+ + + + + + + + +
+ Step 1
+ Fork 本仓库 +
+ Step 2
+ 创建分支
+ git checkout -b feature/amazing-feature +
+ Step 3
+ 提交更改
+ git commit -m 'Add some feature' +
+ Step 4
+ 推送分支
+ git push origin feature/amazing-feature +
+ Step 5
+ 开启 PR +
+
+ +
+ +## 📄 许可证 + +该项目采用 MIT 许可证 - 详情请查看 [LICENSE](LICENSE) 文件。 + +
+ +
+

用 ❤️ 和 Go 制作

+

© 2025 landaiqing

+ +
+
+ + + diff --git a/animation/blink.go b/animation/blink.go new file mode 100644 index 0000000..7452c3a --- /dev/null +++ b/animation/blink.go @@ -0,0 +1,75 @@ +package animation + +import ( + "fmt" + "strings" +) + +// BlinkAnimation 闪烁动画 +type BlinkAnimation struct { + BaseAnimation + BlinkCount int // 闪烁次数 + MinOpacity float64 // 最小透明度 + MaxOpacity float64 // 最大透明度 +} + +// NewBlinkAnimation 创建一个闪烁动画 +func NewBlinkAnimation(targetID string, minOpacity, maxOpacity float64, blinkCount int, duration float64, repeatCount int) *BlinkAnimation { + return &BlinkAnimation{ + BaseAnimation: BaseAnimation{ + Type: Blink, // 闪烁动画类型 + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + BlinkCount: blinkCount, + MinOpacity: minOpacity, + MaxOpacity: maxOpacity, + } +} + +// GenerateSVG 生成闪烁动画的SVG代码 +func (a *BlinkAnimation) GenerateSVG() string { + var sb strings.Builder + + // 创建一个animate元素 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + + if a.Delay > 0 { + sb.WriteString(fmt.Sprintf("begin=\"%gs\" ", a.Delay)) + } + + sb.WriteString("/>\n") + + return sb.String() +} diff --git a/animation/bounce.go b/animation/bounce.go new file mode 100644 index 0000000..3b7e1bc --- /dev/null +++ b/animation/bounce.go @@ -0,0 +1,105 @@ +package animation + +import ( + "fmt" + "strings" +) + +// BounceAnimation 弹跳动画 +type BounceAnimation struct { + BaseAnimation + Property string // 要变换的属性(如 y, transform 等) + From string // 起始值 + To string // 结束值 + BounceCount int // 弹跳次数 +} + +// NewBounceAnimation 创建一个弹跳动画 +func NewBounceAnimation(targetID string, property string, from, to string, bounceCount int, duration float64, repeatCount int) *BounceAnimation { + return &BounceAnimation{ + BaseAnimation: BaseAnimation{ + Type: Bounce, // 弹跳动画类型 + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + Property: property, + From: from, + To: to, + BounceCount: bounceCount, + } +} + +// GenerateSVG 生成弹跳动画的SVG代码 +func (a *BounceAnimation) GenerateSVG() string { + var sb strings.Builder + + // 创建animateTransform元素,使用transform属性 + if a.Property == "transform" { + sb.WriteString(fmt.Sprintf(" 0 && i < a.BounceCount*2 { + keyTime = keyTime + (step * 0.1) // 稍微延长每次弹跳的时间 + } + keyTimes = append(keyTimes, fmt.Sprintf("%.3f", keyTime)) + + if i%2 == 0 { + values = append(values, a.From) + } else { + values = append(values, a.To) + } + } + + // 添加关键帧属性 + sb.WriteString(fmt.Sprintf("values=\"%s\" ", strings.Join(values, ";"))) + sb.WriteString(fmt.Sprintf("keyTimes=\"%s\" ", strings.Join(keyTimes, ";"))) + + // 添加缓动函数 + sb.WriteString("calcMode=\"spline\" ") + sb.WriteString("keySplines=\"") + for i := 0; i < len(values)-1; i++ { + if i > 0 { + sb.WriteString(";") + } + if i%2 == 0 { + // 快速上升 + sb.WriteString("0.2 0 0.8 1") + } else { + // 缓慢下落 + sb.WriteString("0.2 0.8 0.8 1") + } + } + sb.WriteString("\" ") + + if a.RepeatCount < 0 { + sb.WriteString("repeatCount=\"indefinite\" ") + } else if a.RepeatCount > 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + + if a.Delay > 0 { + sb.WriteString(fmt.Sprintf("begin=\"%gs\" ", a.Delay)) + } + + // 添加fill属性 + sb.WriteString("fill=\"freeze\" />") + + return sb.String() +} diff --git a/animation/color.go b/animation/color.go new file mode 100644 index 0000000..f721d19 --- /dev/null +++ b/animation/color.go @@ -0,0 +1,55 @@ +package animation + +import ( + "fmt" + "strings" +) + +// ColorAnimation 颜色变换动画 +type ColorAnimation struct { + BaseAnimation + FromColor string // 起始颜色 + ToColor string // 结束颜色 + Property string // 要变换的属性(fill 或 stroke) +} + +// NewColorAnimation 创建一个颜色变换动画 +func NewColorAnimation(targetID string, property string, fromColor, toColor string, duration float64, repeatCount int) *ColorAnimation { + return &ColorAnimation{ + BaseAnimation: BaseAnimation{ + Type: Color, // 颜色变换动画类型 + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + FromColor: fromColor, + ToColor: toColor, + Property: property, + } +} + +// GenerateSVG 生成颜色变换动画的SVG代码 +func (a *ColorAnimation) GenerateSVG() string { + var sb strings.Builder + + // 创建一个animate元素 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + + if a.Delay > 0 { + sb.WriteString(fmt.Sprintf("begin=\"%gs\" ", a.Delay)) + } + + sb.WriteString("/>\n") + + return sb.String() +} diff --git a/animation/fade.go b/animation/fade.go new file mode 100644 index 0000000..8dd73e0 --- /dev/null +++ b/animation/fade.go @@ -0,0 +1,50 @@ +package animation + +import ( + "fmt" +) + +// FadeAnimation 淡入淡出动画 +type FadeAnimation struct { + BaseAnimation + From string // 起始透明度 + To string // 结束透明度 +} + +// NewFadeAnimation 创建一个淡入淡出动画 +func NewFadeAnimation(targetID string, from, to string, duration float64, repeatCount int) *FadeAnimation { + return &FadeAnimation{ + BaseAnimation: BaseAnimation{ + Type: Fade, + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + From: from, + To: to, + } +} + +// GenerateSVG 生成淡入淡出动画的SVG代码 +func (a *FadeAnimation) GenerateSVG() string { + // 创建一个animate元素,并将其添加到目标元素中 + svg := fmt.Sprintf(" 0 { + svg += fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount) + } + + if a.Delay > 0 { + svg += fmt.Sprintf("begin=\"%gs\" ", a.Delay) + } + + svg += "/>\n" + + return svg +} diff --git a/animation/gradient.go b/animation/gradient.go new file mode 100644 index 0000000..387fdd9 --- /dev/null +++ b/animation/gradient.go @@ -0,0 +1,73 @@ +package animation + +import ( + "fmt" + "strings" +) + +// GradientAnimation 渐变动画 +type GradientAnimation struct { + BaseAnimation + Colors []string // 渐变颜色列表 + Animate bool // 是否添加动画效果 +} + +// NewGradientAnimation 创建一个渐变动画 +func NewGradientAnimation(targetID string, colors []string, duration float64, repeatCount int, animate bool) *GradientAnimation { + return &GradientAnimation{ + BaseAnimation: BaseAnimation{ + Type: Gradient, + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + Colors: colors, + Animate: animate, + } +} + +// GenerateSVG 生成渐变动画的SVG代码 +func (a *GradientAnimation) GenerateSVG() string { + var sb strings.Builder + + // 创建渐变定义 + gradientID := fmt.Sprintf("%s-gradient", a.TargetID) + sb.WriteString(fmt.Sprintf("\n", gradientID)) + + // 添加渐变颜色 + for i, color := range a.Colors { + offset := float64(i) / float64(len(a.Colors)-1) * 100 + sb.WriteString(fmt.Sprintf(" \n", offset, color)) + } + sb.WriteString("\n") + + // 为目标元素添加样式引用 + sb.WriteString(fmt.Sprintf("\n", a.TargetID, gradientID)) + + // 添加动画 + if a.Animate { + // x1 动画 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + sb.WriteString("/>\n") + + // x2 动画 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + sb.WriteString("/>\n") + } + + return sb.String() +} diff --git a/animation/path.go b/animation/path.go new file mode 100644 index 0000000..89c924e --- /dev/null +++ b/animation/path.go @@ -0,0 +1,62 @@ +package animation + +import ( + "fmt" + "strings" +) + +// PathAnimation 路径动画 +type PathAnimation struct { + BaseAnimation + Path string // SVG路径数据 + Rotate string // 是否旋转元素以跟随路径方向 ("auto", "auto-reverse", 或 "0") +} + +// NewPathAnimation 创建一个路径动画 +func NewPathAnimation(targetID string, path string, duration float64, repeatCount int) *PathAnimation { + return &PathAnimation{ + BaseAnimation: BaseAnimation{ + Type: Path, // 路径动画类型 + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + Path: path, + Rotate: "0", // 默认不旋转 + } +} + +// WithRotate 设置是否旋转元素以跟随路径方向 +func (a *PathAnimation) WithRotate(rotate string) *PathAnimation { + a.Rotate = rotate + return a +} + +// GenerateSVG 生成路径动画的SVG代码 +func (a *PathAnimation) GenerateSVG() string { + var sb strings.Builder + + // 创建一个animateMotion元素 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + + if a.Delay > 0 { + sb.WriteString(fmt.Sprintf("begin=\"%gs\" ", a.Delay)) + } + + // 设置旋转属性 + sb.WriteString(fmt.Sprintf("rotate=\"%s\" ", a.Rotate)) + + sb.WriteString("/>\n") + + return sb.String() +} diff --git a/animation/rotate.go b/animation/rotate.go new file mode 100644 index 0000000..b3bfa35 --- /dev/null +++ b/animation/rotate.go @@ -0,0 +1,61 @@ +package animation + +import ( + "fmt" +) + +// RotateAnimation 旋转动画 +type RotateAnimation struct { + BaseAnimation + FromAngle float64 // 起始角度 + ToAngle float64 // 结束角度 + CenterX float64 // 旋转中心X坐标 + CenterY float64 // 旋转中心Y坐标 +} + +// NewRotateAnimation 创建一个旋转动画 +func NewRotateAnimation(targetID string, fromAngle, toAngle float64, duration float64, repeatCount int) *RotateAnimation { + return &RotateAnimation{ + BaseAnimation: BaseAnimation{ + Type: Rotate, + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + FromAngle: fromAngle, + ToAngle: toAngle, + CenterX: 0, + CenterY: 0, + } +} + +// GenerateSVG 生成旋转动画的SVG代码 +func (a *RotateAnimation) GenerateSVG() string { + // 创建一个带有transform-box和transform-origin样式的g元素 + // 这个g元素将包裹目标元素及其相关元素 + svg := fmt.Sprintf("\n") + + // 这里只添加animateTransform元素 + svg += fmt.Sprintf(" 0 { + svg += fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount) + } + + if a.Delay > 0 { + svg += fmt.Sprintf("begin=\"%gs\" ", a.Delay) + } + + svg += "additive=\"sum\" />\n" + + svg += "\n" + + return svg +} diff --git a/animation/transform.go b/animation/transform.go new file mode 100644 index 0000000..ebfc247 --- /dev/null +++ b/animation/transform.go @@ -0,0 +1,52 @@ +package animation + +import ( + "fmt" +) + +// TransformAnimation 变换动画 +type TransformAnimation struct { + BaseAnimation + TransformType string // 变换类型(scale, translate等) + From string // 起始变换值 + To string // 结束变换值 +} + +// NewTransformAnimation 创建一个变换动画 +func NewTransformAnimation(targetID string, transformType string, from, to string, duration float64, repeatCount int) *TransformAnimation { + return &TransformAnimation{ + BaseAnimation: BaseAnimation{ + Type: Transform, + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + TransformType: transformType, + From: from, + To: to, + } +} + +// GenerateSVG 生成变换动画的SVG代码 +func (a *TransformAnimation) GenerateSVG() string { + // 创建一个animateTransform元素,并将其添加到目标元素中 + svg := fmt.Sprintf(" 0 { + svg += fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount) + } + + if a.Delay > 0 { + svg += fmt.Sprintf("begin=\"%gs\" ", a.Delay) + } + + svg += "additive=\"sum\" />\n" + + return svg +} diff --git a/animation/types.go b/animation/types.go new file mode 100644 index 0000000..3c3c50f --- /dev/null +++ b/animation/types.go @@ -0,0 +1,125 @@ +package animation + +import ( + "strings" +) + +// AnimationType 表示动画类型 +type AnimationType string + +// 预定义动画类型常量 +const ( + Rotate AnimationType = "rotate" // 旋转动画 + Gradient AnimationType = "gradient" // 渐变动画 + Transform AnimationType = "transform" // 变换动画 + Fade AnimationType = "fade" // 淡入淡出动画 + Path AnimationType = "path" // 路径动画 + Color AnimationType = "color" // 颜色变换动画 + Bounce AnimationType = "bounce" // 弹跳动画 + Wave AnimationType = "wave" // 波浪动画 + Blink AnimationType = "blink" // 闪烁动画 +) + +// Animation 表示一个SVG动画接口 +type Animation interface { + // GenerateSVG 生成动画的SVG代码 + GenerateSVG() string + // GetTargetID 获取目标元素ID + GetTargetID() string +} + +// BaseAnimation 基础动画结构,包含所有动画共有的属性 +type BaseAnimation struct { + Type AnimationType // 动画类型 + Duration float64 // 动画持续时间(秒) + RepeatCount int // 重复次数,-1表示无限重复 + Delay float64 // 延迟时间(秒) + TargetID string // 目标元素ID + Attributes map[string]string // 动画属性 +} + +// GetTargetID 获取目标元素ID +func (a *BaseAnimation) GetTargetID() string { + return a.TargetID +} + +// Manager 动画管理器,负责管理所有动画 +type Manager struct { + animations []Animation +} + +// GetAnimations 获取所有动画 +func (m *Manager) GetAnimations() []Animation { + return m.animations +} + +// NewAnimationManager 创建一个新的动画管理器 +func NewAnimationManager() *Manager { + return &Manager{ + animations: make([]Animation, 0), + } +} + +// AddAnimation 添加一个动画 +func (m *Manager) AddAnimation(animation Animation) { + m.animations = append(m.animations, animation) +} + +// GenerateSVGAnimations 生成SVG动画代码 +func (m *Manager) GenerateSVGAnimations() string { + if len(m.animations) == 0 { + return "" + } + + var sb strings.Builder + + // 添加SVG命名空间声明 + sb.WriteString("\n") + + // 用于存储需要放在defs中的定义 + var defsContent strings.Builder + // 用于存储需要直接添加到SVG中的动画元素 + var animationsContent strings.Builder + // 用于存储旋转动画的映射,键为目标元素ID + rotateAnimations := make(map[string]string) + + // 处理所有动画 + for _, anim := range m.animations { + svgCode := anim.GenerateSVG() + if svgCode == "" { + continue + } + + // 根据动画类型决定放置位置 + switch a := anim.(type) { + case *GradientAnimation: + // 渐变定义需要放在defs中 + defsContent.WriteString(svgCode) + case *RotateAnimation: + // 旋转动画需要包裹目标元素,先存储起来 + // 提取animateTransform标签 + if start := strings.Index(svgCode, ""); end != -1 { + rotateAnimations[a.GetTargetID()] = svgCode[start : start+end+2] + } + } + default: + // 其他动画元素直接添加到SVG中 + animationsContent.WriteString(svgCode) + } + } + + // 只有当存在需要放在defs中的内容时才添加defs标签 + if defsContent.Len() > 0 { + sb.WriteString(defsContent.String()) + sb.WriteString("\n") + } else { + // 如果没有需要放在defs中的内容,则不添加defs标签 + sb.Reset() + } + + // 添加直接放置的动画元素 + sb.WriteString(animationsContent.String()) + + return sb.String() +} diff --git a/animation/wave.go b/animation/wave.go new file mode 100644 index 0000000..941012f --- /dev/null +++ b/animation/wave.go @@ -0,0 +1,81 @@ +package animation + +import ( + "fmt" + "math" + "strings" +) + +// WaveAnimation 波浪动画 +type WaveAnimation struct { + BaseAnimation + Amplitude float64 // 波浪振幅 + Frequency float64 // 波浪频率 + Direction string // 波浪方向 ("horizontal" 或 "vertical") +} + +// NewWaveAnimation 创建一个波浪动画 +func NewWaveAnimation(targetID string, amplitude, frequency float64, direction string, duration float64, repeatCount int) *WaveAnimation { + return &WaveAnimation{ + BaseAnimation: BaseAnimation{ + Type: Wave, // 波浪动画类型 + Duration: duration, + RepeatCount: repeatCount, + Delay: 0, + TargetID: targetID, + Attributes: make(map[string]string), + }, + Amplitude: amplitude, + Frequency: frequency, + Direction: direction, + } +} + +// GenerateSVG 生成波浪动画的SVG代码 +func (a *WaveAnimation) GenerateSVG() string { + var sb strings.Builder + + // 生成波浪路径 + path := a.generateWavePath() + + // 创建一个animateMotion元素 + sb.WriteString(fmt.Sprintf(" 0 { + sb.WriteString(fmt.Sprintf("repeatCount=\"%d\" ", a.RepeatCount)) + } + + if a.Delay > 0 { + sb.WriteString(fmt.Sprintf("begin=\"%gs\" ", a.Delay)) + } + + sb.WriteString("/>\n") + + return sb.String() +} + +// generateWavePath 生成波浪路径 +func (a *WaveAnimation) generateWavePath() string { + var path strings.Builder + path.WriteString("M0,0 ") + + // 生成正弦波路径 + points := 20 // 路径点数量 + for i := 0; i <= points; i++ { + x := float64(i) / float64(points) * 100 // 0-100 范围 + // 计算正弦波 y 值 + y := a.Amplitude * math.Sin(a.Frequency*x*math.Pi/180) + + if a.Direction == "horizontal" { + path.WriteString(fmt.Sprintf("L%g,%g ", x, y)) + } else { // vertical + path.WriteString(fmt.Sprintf("L%g,%g ", y, x)) + } + } + + return path.String() +} diff --git a/assets/animation.svg b/assets/animation.svg new file mode 100644 index 0000000..f732183 --- /dev/null +++ b/assets/animation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/api.svg b/assets/api.svg new file mode 100644 index 0000000..beb1777 --- /dev/null +++ b/assets/api.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..e8a06ee Binary files /dev/null and b/assets/background.png differ diff --git a/assets/base_use.svg b/assets/base_use.svg new file mode 100644 index 0000000..039b135 --- /dev/null +++ b/assets/base_use.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/cache.svg b/assets/cache.svg new file mode 100644 index 0000000..6bf0fcf --- /dev/null +++ b/assets/cache.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/convert.svg b/assets/convert.svg new file mode 100644 index 0000000..3a29ca7 --- /dev/null +++ b/assets/convert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/customize.svg b/assets/customize.svg new file mode 100644 index 0000000..3892558 --- /dev/null +++ b/assets/customize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/diversify.svg b/assets/diversify.svg new file mode 100644 index 0000000..4cd6a0e --- /dev/null +++ b/assets/diversify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/example_avatar.svg b/assets/example_avatar.svg new file mode 100644 index 0000000..6fef43e --- /dev/null +++ b/assets/example_avatar.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/example_avatar_1.svg b/assets/example_avatar_1.svg new file mode 100644 index 0000000..790510a --- /dev/null +++ b/assets/example_avatar_1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/example_avatar_2.svg b/assets/example_avatar_2.svg new file mode 100644 index 0000000..cbb4d47 --- /dev/null +++ b/assets/example_avatar_2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/example_avatar_3.svg b/assets/example_avatar_3.svg new file mode 100644 index 0000000..4ae5f89 --- /dev/null +++ b/assets/example_avatar_3.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/example_avatar_4.svg b/assets/example_avatar_4.svg new file mode 100644 index 0000000..36517e5 --- /dev/null +++ b/assets/example_avatar_4.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/example_avatar_5.svg b/assets/example_avatar_5.svg new file mode 100644 index 0000000..ae14518 --- /dev/null +++ b/assets/example_avatar_5.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/golang_logo.gif b/assets/golang_logo.gif new file mode 100644 index 0000000..fc671e2 Binary files /dev/null and b/assets/golang_logo.gif differ diff --git a/assets/performance.svg b/assets/performance.svg new file mode 100644 index 0000000..e17bf7c --- /dev/null +++ b/assets/performance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pixel_planet.gif b/assets/pixel_planet.gif new file mode 100644 index 0000000..0290190 Binary files /dev/null and b/assets/pixel_planet.gif differ diff --git a/assets/pixel_planet_2.gif b/assets/pixel_planet_2.gif new file mode 100644 index 0000000..0837f07 Binary files /dev/null and b/assets/pixel_planet_2.gif differ diff --git a/assets/random.svg b/assets/random.svg new file mode 100644 index 0000000..d5b7c98 --- /dev/null +++ b/assets/random.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/step_1.svg b/assets/step_1.svg new file mode 100644 index 0000000..a87d4eb --- /dev/null +++ b/assets/step_1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/step_2.svg b/assets/step_2.svg new file mode 100644 index 0000000..0e00aaa --- /dev/null +++ b/assets/step_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/step_3.svg b/assets/step_3.svg new file mode 100644 index 0000000..bc0cac7 --- /dev/null +++ b/assets/step_3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/step_4.svg b/assets/step_4.svg new file mode 100644 index 0000000..134b996 --- /dev/null +++ b/assets/step_4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/step_5.svg b/assets/step_5.svg new file mode 100644 index 0000000..5f16f61 --- /dev/null +++ b/assets/step_5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/style.svg b/assets/style.svg new file mode 100644 index 0000000..77d1c22 --- /dev/null +++ b/assets/style.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/theme.svg b/assets/theme.svg new file mode 100644 index 0000000..cc5bb09 --- /dev/null +++ b/assets/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..4f9d9d5 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,209 @@ +#
🚀 PixelNebula 基准测试
+ + + [英文](README_EN.md) | 中文 + + +## 📊 简介 + +本目录包含了 PixelNebula 库的全面基准测试,用于测量和分析库在各种操作场景下的性能表现。这些测试可以帮助我们识别潜在的性能瓶颈,并指导后续的优化工作。 + +
+ +## 🧪 测试内容 + +
+ + + + + + + + +
+ 基本生成
+ 基本头像生成 +
+ 样式和主题
+ 样式和主题 +
+ 动画效果
+ 动画效果 +
+ 缓存系统
+ 缓存系统 +
+ 并发性能
+ 并发与内存 +
+
+ +基准测试涵盖了 PixelNebula 的以下核心功能: + +### 1. 🖼️ 基本头像生成 (`basic_benchmark_test.go`) +- 普通头像生成 +- 无环境头像生成 +- 不同尺寸头像生成 +- 相同ID多次生成 + +### 2. 🎨 样式和主题 (`style_theme_benchmark_test.go`) +- 不同样式的性能对比 +- 不同主题的性能对比 +- 自定义主题 +- 样式与主题组合 + +### 3. ✨ 动画效果 (`animation_benchmark_test.go`) +- 旋转动画 +- 渐变动画 +- 淡入淡出动画 +- 变换动画 +- 颜色动画 +- 多个动画组合 + +### 4. 💾 缓存系统 (`cache_benchmark_test.go`) +- 无缓存 vs. 默认缓存 +- 不同缓存大小 +- 缓存压缩效果 +- 不同过期时间配置 + +### 5. ⚡ 并发与内存使用 (`concurrency_memory_benchmark_test.go`) +- 不同并发级别的性能 +- 共享实例的并发性能 +- 各种操作的内存占用分析 + +
+ +## 🚀 运行基准测试 + +### 运行所有测试 + +```bash +cd benchmark +go test -bench=. -benchmem +``` + +### 运行特定测试组 + +
+ + + + + + +
+
+

🏃 基本测试

+
go test -bench=BenchmarkBasic -benchmem
+
+
+
+

💾 缓存测试

+
go test -bench=BenchmarkCache -benchmem
+
+
+
+

✨ 动画测试

+
go test -bench=BenchmarkAnimation -benchmem
+
+
+
+ +### 高级配置 + +
+ + + + + +
+
+

⚙️ 设置CPU计数

+
go test -bench=. -benchmem -cpu=1,2,4,8
+
+
+
+

⏱️ 设置迭代次数和时间

+
go test -bench=. -benchmem -count=5 -benchtime=5s
+
+
+
+ +
+ +## 📈 测试结果分析 + +运行测试后,结果会以如下格式显示: + +``` +BenchmarkBasicAvatarGeneration-8 5000 234567 ns/op 12345 B/op 123 allocs/op +``` + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
组成部分描述
BenchmarkBasicAvatarGeneration-8测试名称,8表示使用8个CPU
5000测试运行的迭代次数
234567 ns/op每次操作的平均耗时(纳秒)
12345 B/op每次操作的平均内存分配(字节)
123 allocs/op每次操作的平均内存分配次数
+
+ +
+ +## 📝 添加基准测试 + +在添加新功能时,建议同时添加相应的基准测试: + +1. **创建测试函数**:为特定功能创建新的测试函数,命名为 `BenchmarkXXX` +2. **重置计时器**:使用 `b.ResetTimer()` 在准备工作完成后重置计时器 +3. **创建子测试**:使用 `b.Run()` 创建子测试以对比不同变量 +4. **使用迭代计数**:使用 `b.N` 作为迭代次数以确保统计准确性 + +
+

示例:

+ +```go +func BenchmarkMyFeature(b *testing.B) { + // 准备代码 + pn := pixelnebula.NewPixelNebula() + + // 在实际基准测试前重置计时器 + b.ResetTimer() + + // 运行基准测试 + for i := 0; i < b.N; i++ { + // 要测试的代码 + pn.MyFeature() + } +} +``` +
+ +
+ +
+

更多信息,请查看 PixelNebula 文档和示例。

+

© 2024 landaiqing

+
\ No newline at end of file diff --git a/benchmark/README_EN.md b/benchmark/README_EN.md new file mode 100644 index 0000000..6556b6d --- /dev/null +++ b/benchmark/README_EN.md @@ -0,0 +1,209 @@ +#
🚀 PixelNebula Benchmarks
+ + + [中文](README.md) | English + + +## 📊 Introduction + +This directory contains comprehensive benchmark tests for the PixelNebula library, designed to measure and analyze performance across various operational scenarios. These tests help identify potential performance bottlenecks and guide subsequent optimization efforts. + +
+ +## 🧪 Test Content + +
+ + + + + + + + +
+ Basic Generation
+ Basic Avatar Generation +
+ Styles & Themes
+ Styles & Themes +
+ Animations
+ Animation Effects +
+ Cache System
+ Cache System +
+ Concurrency
+ Concurrency & Memory +
+
+ +The benchmarks cover the following core functionalities of PixelNebula: + +### 1. 🖼️ Basic Avatar Generation (`basic_benchmark_test.go`) +- Regular avatar generation +- No-environment avatar generation +- Avatar generation with different sizes +- Multiple generations with the same ID + +### 2. 🎨 Styles and Themes (`style_theme_benchmark_test.go`) +- Performance comparison between different styles +- Performance comparison between different themes +- Custom themes +- Style and theme combinations + +### 3. ✨ Animation Effects (`animation_benchmark_test.go`) +- Rotation animation +- Gradient animation +- Fade-in/out animation +- Transform animation +- Color animation +- Multiple animation combinations + +### 4. 💾 Cache System (`cache_benchmark_test.go`) +- No cache vs. default cache +- Different cache sizes +- Cache compression effects +- Different expiry time configurations + +### 5. ⚡ Concurrency and Memory Usage (`concurrency_memory_benchmark_test.go`) +- Performance at different concurrency levels +- Concurrent performance with shared instances +- Memory usage analysis for various operations + +
+ +## 🚀 Running Benchmarks + +### Run All Tests + +```bash +cd benchmark +go test -bench=. -benchmem +``` + +### Run Specific Test Groups + +
+ + + + + + +
+
+

🏃 Basic Tests

+
go test -bench=BenchmarkBasic -benchmem
+
+
+
+

💾 Cache Tests

+
go test -bench=BenchmarkCache -benchmem
+
+
+
+

✨ Animation Tests

+
go test -bench=BenchmarkAnimation -benchmem
+
+
+
+ +### Advanced Configuration + +
+ + + + + +
+
+

⚙️ Set CPU Count

+
go test -bench=. -benchmem -cpu=1,2,4,8
+
+
+
+

⏱️ Set Iteration Count and Duration

+
go test -bench=. -benchmem -count=5 -benchtime=5s
+
+
+
+ +
+ +## 📈 Test Result Analysis + +After running the tests, results will be displayed in the following format: + +``` +BenchmarkBasicAvatarGeneration-8 5000 234567 ns/op 12345 B/op 123 allocs/op +``` + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentDescription
BenchmarkBasicAvatarGeneration-8Test name, 8 indicates using 8 CPUs
5000Number of iterations the test ran
234567 ns/opAverage time per operation (nanoseconds)
12345 B/opAverage memory allocation per operation (bytes)
123 allocs/opAverage number of memory allocations per operation
+
+ +
+ +## 📝 Adding Benchmark Tests + +When adding new features, it's recommended to add corresponding benchmark tests: + +1. **Create a Test Function**: Create a new test function for the specific feature, named `BenchmarkXXX` +2. **Reset Timer**: Use `b.ResetTimer()` to reset the timer after setup work is complete +3. **Create Sub-tests**: Use `b.Run()` to create sub-tests for comparing different variables +4. **Use Iteration Count**: Use `b.N` as the iteration count to ensure statistical accuracy + +
+

Example:

+ +```go +func BenchmarkMyFeature(b *testing.B) { + // Setup code + pn := pixelnebula.NewPixelNebula() + + // Reset timer before the actual benchmark + b.ResetTimer() + + // Run the benchmark + for i := 0; i < b.N; i++ { + // Code to benchmark + pn.MyFeature() + } +} +``` +
+ +
+ +
+

For more information, check out the PixelNebula documentation and examples.

+

© 2024 landaiqing

+
\ No newline at end of file diff --git a/benchmark/animation_benchmark_test.go b/benchmark/animation_benchmark_test.go new file mode 100644 index 0000000..03395d8 --- /dev/null +++ b/benchmark/animation_benchmark_test.go @@ -0,0 +1,132 @@ +package benchmark + +import ( + "testing" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// BenchmarkRotateAnimation 测试旋转动画的性能 +func BenchmarkRotateAnimation(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithRotateAnimation("env", 0, 360, 10, 1) // 单次旋转 + + _, err := pn.Generate("benchmark-rotate", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkGradientAnimation 测试渐变动画的性能 +func BenchmarkGradientAnimation(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithGradientAnimation("head", []string{"#ff0000", "#00ff00", "#0000ff"}, 5, 1, true) + + _, err := pn.Generate("benchmark-gradient", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkFadeAnimation 测试淡入淡出动画的性能 +func BenchmarkFadeAnimation(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithFadeAnimation("eyes", "1", "0.3", 2, 1) + + _, err := pn.Generate("benchmark-fade", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkTransformAnimation 测试变换动画的性能 +func BenchmarkTransformAnimation(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithTransformAnimation("mouth", "scale", "1 1", "1.1 1.1", 1.5, 1) + + _, err := pn.Generate("benchmark-transform", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkColorAnimation 测试颜色变换动画的性能 +func BenchmarkColorAnimation(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithColorAnimation("clo", "fill", "#ff0000", "#0000ff", 3, 1) + + _, err := pn.Generate("benchmark-color", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkMultipleAnimations 测试多个动画组合的性能 +func BenchmarkMultipleAnimations(b *testing.B) { + animationCounts := []int{1, 2, 3, 5} + + for _, count := range animationCounts { + b.Run("Animations_"+Itoa(count), func(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // 根据数量添加不同的动画 + if count >= 1 { + pn.WithRotateAnimation("env", 0, 360, 10, 1) + } + if count >= 2 { + pn.WithFadeAnimation("eyes", "1", "0.3", 2, 1) + } + if count >= 3 { + pn.WithTransformAnimation("mouth", "scale", "1 1", "1.1 1.1", 1.5, 1) + } + if count >= 4 { + pn.WithColorAnimation("clo", "fill", "#ff0000", "#0000ff", 3, 1) + } + if count >= 5 { + pn.WithGradientAnimation("head", []string{"#ff0000", "#00ff00", "#0000ff"}, 5, 1, true) + } + + _, err := pn.Generate("benchmark-multi-"+Itoa(count), false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/benchmark/basic_benchmark_test.go b/benchmark/basic_benchmark_test.go new file mode 100644 index 0000000..81b6b55 --- /dev/null +++ b/benchmark/basic_benchmark_test.go @@ -0,0 +1,88 @@ +package benchmark + +import ( + "strconv" + "testing" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// BenchmarkBasicAvatarGeneration 测试基本头像生成性能 +func BenchmarkBasicAvatarGeneration(b *testing.B) { + // 重置计时器 + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // 生成SVG + _, err := pn.Generate("benchmark-id", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkAvatarWithNoEnvironment 测试无环境头像生成性能 +func BenchmarkAvatarWithNoEnvironment(b *testing.B) { + // 重置计时器 + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // 生成无环境SVG + _, err := pn.Generate("benchmark-id", true).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkDifferentSizes 测试不同大小头像生成性能 +func BenchmarkDifferentSizes(b *testing.B) { + sizes := []int{100, 200, 400, 800} + + for _, size := range sizes { + b.Run("Size_"+Itoa(size), func(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(size, size) + + _, err := pn.Generate("benchmark-size-"+Itoa(size), false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkIDReuse 测试多次使用相同ID生成头像的性能(不使用缓存) +func BenchmarkIDReuse(b *testing.B) { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := pn.Generate("fixed-benchmark-id", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// Itoa 简单的整数转字符串函数 +func Itoa(n int) string { + return strconv.Itoa(n) +} diff --git a/benchmark/cache_benchmark_test.go b/benchmark/cache_benchmark_test.go new file mode 100644 index 0000000..0e05059 --- /dev/null +++ b/benchmark/cache_benchmark_test.go @@ -0,0 +1,155 @@ +package benchmark + +import ( + "testing" + "time" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/cache" + "github.com/landaiqing/go-pixelnebula/style" +) + +// BenchmarkDefaultCacheVsNoCache 对比有无默认缓存的性能差异 +func BenchmarkDefaultCacheVsNoCache(b *testing.B) { + // 不使用缓存的基准测试 + b.Run("NoCache", func(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + _, err := pn.Generate("benchmark-cache-test", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + + // 使用默认缓存的基准测试 + b.Run("DefaultCache", func(b *testing.B) { + // 创建一个带默认缓存的实例 + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithDefaultCache() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := pn.Generate("benchmark-cache-test", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) +} + +// BenchmarkCacheSizes 测试不同缓存大小对性能的影响 +func BenchmarkCacheSizes(b *testing.B) { + cacheSizes := []int{10, 100, 1000} + + for _, size := range cacheSizes { + b.Run("CacheSize_"+Itoa(size), func(b *testing.B) { + // 创建自定义缓存配置 + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithCache(cache.CacheOptions{ + Size: size, + Expiration: 3600 * time.Second, + }) + + // 预热缓存,生成一些不同的头像 + for i := 0; i < size/2; i++ { + _, _ = pn.Generate("preload-"+Itoa(i), false).ToSVG() + } + + b.ResetTimer() + + // 测试缓存命中和未命中的混合场景 + for i := 0; i < b.N; i++ { + id := "benchmark-" + Itoa(i%size) // 循环使用ID,确保部分缓存命中 + _, err := pn.Generate(id, false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkCacheCompression 测试缓存压缩对性能的影响 +func BenchmarkCacheCompression(b *testing.B) { + compressionLevels := []struct { + name string + level int + }{ + {"NoCompression", 0}, + {"LowCompression", 3}, + {"MediumCompression", 6}, + {"HighCompression", 9}, + } + + for _, cl := range compressionLevels { + b.Run(cl.name, func(b *testing.B) { + // 创建带压缩缓存的实例 + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithDefaultCache() + + if cl.level > 0 { + pn.WithCompression(cache.CompressOptions{ + Enabled: true, + Level: cl.level, + MinSizeBytes: 100, + }) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := pn.Generate("benchmark-compress", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkCacheExpiry 测试不同缓存过期时间的性能影响 +func BenchmarkCacheExpiry(b *testing.B) { + expiryTimes := []struct { + name string + time time.Duration + }{ + {"Short_1m", 1 * time.Minute}, + {"Medium_1h", 1 * time.Hour}, + {"Long_24h", 24 * time.Hour}, + } + + for _, et := range expiryTimes { + b.Run(et.name, func(b *testing.B) { + // 创建带自定义过期时间的缓存实例 + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithCache(cache.CacheOptions{ + Size: 100, + Expiration: et.time, + }) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := pn.Generate("benchmark-expiry", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/benchmark/concurrency_memory_benchmark_test.go b/benchmark/concurrency_memory_benchmark_test.go new file mode 100644 index 0000000..3ad6dc8 --- /dev/null +++ b/benchmark/concurrency_memory_benchmark_test.go @@ -0,0 +1,138 @@ +package benchmark + +import ( + "sync" + "testing" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// BenchmarkConcurrentGeneration 测试并发生成头像的性能 +func BenchmarkConcurrentGeneration(b *testing.B) { + concurrencyCounts := []int{1, 2, 4, 8, 16} + + for _, count := range concurrencyCounts { + b.Run("Concurrent_"+Itoa(count), func(b *testing.B) { + b.ResetTimer() + + // 将总迭代次数调整为b.N,确保可比较性 + b.SetParallelism(count) + b.RunParallel(func(pb *testing.PB) { + counter := 0 + for pb.Next() { + counter++ + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithDefaultCache() + + _, err := pn.Generate("benchmark-concurrent-"+Itoa(counter), false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + }) + } +} + +// BenchmarkConcurrentWithSharedInstance 测试使用共享实例进行并发生成的性能 +func BenchmarkConcurrentWithSharedInstance(b *testing.B) { + concurrencyCounts := []int{1, 2, 4, 8, 16} + + for _, count := range concurrencyCounts { + b.Run("SharedInstance_"+Itoa(count), func(b *testing.B) { + // 创建一个共享实例 + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithDefaultCache() + + // 创建互斥锁保护共享实例 + var mu sync.Mutex + + b.ResetTimer() + b.SetParallelism(count) + b.RunParallel(func(pb *testing.PB) { + counter := 0 + for pb.Next() { + counter++ + // 锁定共享实例 + mu.Lock() + _, err := pn.Generate("benchmark-shared-"+Itoa(counter), false).ToSVG() + mu.Unlock() + if err != nil { + b.Fatal(err) + } + } + }) + }) + } +} + +// BenchmarkMemoryUsage 测试不同操作的内存使用情况 +func BenchmarkMemoryUsage(b *testing.B) { + // 注意: 这个基准测试主要关注内存分配统计, + // Go 的基准测试框架会自动收集并报告内存统计数据 + + // 测试基本头像生成的内存使用 + b.Run("BasicGeneration", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + _, err := pn.Generate("memory-basic", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + + // 测试添加动画的内存使用 + b.Run("WithAnimations", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithRotateAnimation("env", 0, 360, 10, 1) + pn.WithFadeAnimation("eyes", "1", "0.3", 2, 1) + _, err := pn.Generate("memory-animations", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + + // 测试缓存的内存使用 + b.Run("WithCache", func(b *testing.B) { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithDefaultCache() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := pn.Generate("memory-cache", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + + // 测试大尺寸头像的内存使用 + b.Run("LargeSize", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(1000, 1000) + _, err := pn.Generate("memory-large", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/benchmark/style_theme_benchmark_test.go b/benchmark/style_theme_benchmark_test.go new file mode 100644 index 0000000..52a9883 --- /dev/null +++ b/benchmark/style_theme_benchmark_test.go @@ -0,0 +1,126 @@ +package benchmark + +import ( + "testing" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" + "github.com/landaiqing/go-pixelnebula/theme" +) + +// BenchmarkDifferentStyles 测试不同风格的生成性能 +func BenchmarkDifferentStyles(b *testing.B) { + styles := []struct { + name string + style style.StyleType + }{ + {"GirlStyle", style.GirlStyle}, + {"AteamStyle", style.AteamStyle}, + {"BlondStyle", style.BlondStyle}, + {"FirehairStyle", style.FirehairStyle}, + } + + for _, s := range styles { + b.Run(s.name, func(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(s.style) + pn.WithSize(231, 231) + + _, err := pn.Generate("benchmark-style-"+s.name, false).ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkDifferentThemes 测试不同主题的生成性能 +func BenchmarkDifferentThemes(b *testing.B) { + // 假设有5个内置主题索引 + themeCount := 5 + + for i := 0; i < themeCount; i++ { + b.Run("Theme_"+Itoa(i), func(b *testing.B) { + b.ResetTimer() + + for j := 0; j < b.N; j++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + _, err := pn.Generate("benchmark-theme-"+Itoa(i), false).SetTheme(i).Build().ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// BenchmarkCustomTheme 测试自定义主题的性能 +func BenchmarkCustomTheme(b *testing.B) { + // 创建一个自定义主题 + customTheme := []theme.Theme{ + { + theme.ThemePart{ + "env": []string{"#f0f0f0", "#e0e0e0"}, + "head": []string{"#ffd699"}, + "eyes": []string{"#555555", "#ffffff"}, + "mouth": []string{"#ff6b6b"}, + "top": []string{"#6b5b95", "#6b5b95"}, + "clo": []string{"#88b04b"}, + }, + }, + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + pn.WithCustomizeTheme(customTheme) + + _, err := pn.Generate("benchmark-custom-theme", false).ToSVG() + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkStyleThemeCombinations 测试不同风格和主题组合的性能 +func BenchmarkStyleThemeCombinations(b *testing.B) { + styles := []style.StyleType{style.GirlStyle, style.AsianStyle} + themes := []int{0, 1, 2} + + for _, s := range styles { + for _, t := range themes { + styleName := "Unknown" + switch s { + case style.GirlStyle: + styleName = "Girl" + case style.AsianStyle: + styleName = "Asian" + } + + b.Run(styleName+"_Theme"+Itoa(t), func(b *testing.B) { + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pn := pixelnebula.NewPixelNebula() + pn.WithStyle(s) + pn.WithSize(231, 231) + + _, err := pn.Generate("benchmark-combo", false).SetTheme(t).Build().ToSVG() + if err != nil { + b.Fatal(err) + } + } + }) + } + } +} diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..f289df8 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,313 @@ +package cache + +import ( + "container/list" + "sync" + "time" +) + +// CacheOptions 缓存配置选项 +type CacheOptions struct { + Enabled bool // 是否启用缓存 + Size int // 缓存大小,0表示无限制 + Expiration time.Duration // 缓存项过期时间,0表示永不过期 + EvictionType string // 缓存淘汰策略,支持"lru"(最近最少使用)和"fifo"(先进先出) + Compression CompressOptions // 压缩选项 + Monitoring MonitorOptions // 监控选项 +} + +// DefaultCacheOptions 默认缓存配置 +var DefaultCacheOptions = CacheOptions{ + Enabled: true, + Size: 100, // 默认缓存100个SVG + Expiration: time.Hour, // 默认缓存项过期时间为1小时 + EvictionType: "lru", // 默认使用LRU淘汰策略 + Compression: DefaultCompressOptions, // 默认压缩选项 + Monitoring: DefaultMonitorOptions, // 默认监控选项 +} + +// CacheKey 缓存键结构 +type CacheKey struct { + Id string + SansEnv bool + Theme int + Part int +} + +// CacheItem 缓存项结构 +type CacheItem struct { + SVG string // SVG内容 + Compressed []byte // 压缩后的SVG数据 + IsCompressed bool // 是否已压缩 + CreatedAt time.Time // 创建时间 + LastUsed time.Time // 最后使用时间 +} + +// PNCache SVG缓存结构 +type PNCache struct { + Options CacheOptions + Items map[CacheKey]*list.Element // 存储缓存项的映射 + EvictionList *list.List // 用于实现LRU/FIFO的双向链表 + Mutex sync.RWMutex + Hits int // 缓存命中次数 + Misses int // 缓存未命中次数 + Monitor *Monitor // 缓存监控器 +} + +// NewCache 创建一个新的缓存实例 +func NewCache(options CacheOptions) *PNCache { + cache := &PNCache{ + Options: options, + Items: make(map[CacheKey]*list.Element), + EvictionList: list.New(), + Hits: 0, + Misses: 0, + } + + // 如果启用了监控,创建并启动监控器 + if options.Monitoring.Enabled { + cache.Monitor = NewMonitor(cache, options.Monitoring) + cache.Monitor.Start() + } + + return cache +} + +// NewDefaultCache 使用默认配置创建一个新的缓存实例 +func NewDefaultCache() *PNCache { + return NewCache(DefaultCacheOptions) +} + +// Get 从缓存中获取SVG +func (c *PNCache) Get(key CacheKey) (string, bool) { + if !c.Options.Enabled { + c.Misses++ + return "", false + } + + c.Mutex.Lock() // 使用写锁以便更新LRU信息 + defer c.Mutex.Unlock() + + element, found := c.Items[key] + if !found { + c.Misses++ + return "", false + } + + // 获取缓存项 + cacheItem := element.Value.(*CacheItem) + + // 检查是否过期 + if c.Options.Expiration > 0 { + if time.Since(cacheItem.CreatedAt) > c.Options.Expiration { + // 删除过期项 + c.EvictionList.Remove(element) + delete(c.Items, key) + c.Misses++ + return "", false + } + } + + // 更新LRU信息 + if c.Options.EvictionType == "lru" { + cacheItem.LastUsed = time.Now() + c.EvictionList.MoveToFront(element) + } + + c.Hits++ + + // 如果数据已压缩,需要解压缩 + if cacheItem.IsCompressed { + svg, err := DecompressSVG(cacheItem.Compressed, true) + if err != nil { + // 解压失败,返回未压缩的原始数据 + return cacheItem.SVG, true + } + return svg, true + } + + return cacheItem.SVG, true +} + +// Set 将SVG存入缓存 +func (c *PNCache) Set(key CacheKey, svg string) { + if !c.Options.Enabled { + return + } + + c.Mutex.Lock() + defer c.Mutex.Unlock() + + // 尝试压缩SVG数据 + var compressed []byte + var isCompressed bool + + // 如果启用了压缩,尝试压缩SVG + if c.Options.Compression.Enabled { + // 首先优化SVG + optimizedSVG := OptimizeSVG(svg) + + // 然后压缩 + compressed, isCompressed = CompressSVG(optimizedSVG, c.Options.Compression) + + // 如果压缩成功,使用优化后的SVG + if isCompressed { + svg = optimizedSVG + } + } + + // 检查是否已存在 + if element, exists := c.Items[key]; exists { + // 更新现有项 + cacheItem := element.Value.(*CacheItem) + cacheItem.SVG = svg + cacheItem.Compressed = compressed + cacheItem.IsCompressed = isCompressed + cacheItem.LastUsed = time.Now() + cacheItem.CreatedAt = time.Now() + + // 如果使用LRU策略,将项移到链表前端 + if c.Options.EvictionType == "lru" { + c.EvictionList.MoveToFront(element) + } + return + } + + // 如果达到大小限制,需要淘汰一个项 + if c.Options.Size > 0 && len(c.Items) >= c.Options.Size { + c.evictItem() + } + + // 创建新的缓存项 + now := time.Now() + cacheItem := &CacheItem{ + SVG: svg, + Compressed: compressed, + IsCompressed: isCompressed, + CreatedAt: now, + LastUsed: now, + } + + // 添加到链表和映射 + element := c.EvictionList.PushFront(cacheItem) + c.Items[key] = element +} + +// evictItem 根据淘汰策略移除一个缓存项 +func (c *PNCache) evictItem() { + if c.EvictionList.Len() == 0 { + return + } + + // 获取要淘汰的元素 + var element *list.Element + switch c.Options.EvictionType { + case "lru": + // LRU策略:移除链表尾部元素(最近最少使用) + element = c.EvictionList.Back() + default: + // 默认使用FIFO策略:移除链表尾部元素(最先添加) + element = c.EvictionList.Back() + } + + if element != nil { + // 从链表中移除 + c.EvictionList.Remove(element) + + // 从映射中找到并删除对应的键 + for k, v := range c.Items { + if v == element { + delete(c.Items, k) + break + } + } + } +} + +// Clear 清空缓存 +func (c *PNCache) Clear() { + c.Mutex.Lock() + defer c.Mutex.Unlock() + + c.Items = make(map[CacheKey]*list.Element) + c.EvictionList = list.New() + c.Hits = 0 + c.Misses = 0 +} + +// Size 返回当前缓存项数量 +func (c *PNCache) Size() int { + c.Mutex.RLock() + defer c.Mutex.RUnlock() + + return len(c.Items) +} + +// Stats 返回缓存统计信息 +func (c *PNCache) Stats() (Hits, Misses int, hitRate float64) { + c.Mutex.RLock() + defer c.Mutex.RUnlock() + + Hits = c.Hits + Misses = c.Misses + total := Hits + Misses + if total > 0 { + hitRate = float64(Hits) / float64(total) + } + return +} + +// RemoveExpired 移除所有过期的缓存项 +func (c *PNCache) RemoveExpired() int { + if c.Options.Expiration <= 0 { + return 0 + } + + c.Mutex.Lock() + defer c.Mutex.Unlock() + + count := 0 + now := time.Now() + + // 遍历所有缓存项,检查是否过期 + for key, element := range c.Items { + cacheItem := element.Value.(*CacheItem) + if now.Sub(cacheItem.CreatedAt) > c.Options.Expiration { + // 从链表中移除 + c.EvictionList.Remove(element) + // 从映射中删除 + delete(c.Items, key) + count++ + } + } + + return count +} + +// GetOptions 获取当前缓存选项 +func (c *PNCache) GetOptions() CacheOptions { + c.Mutex.RLock() + defer c.Mutex.RUnlock() + + return c.Options +} + +// UpdateOptions 更新缓存选项 +func (c *PNCache) UpdateOptions(options CacheOptions) { + c.Mutex.Lock() + defer c.Mutex.Unlock() + + // 更新选项 + c.Options = options + + // 如果新的缓存大小小于当前项数,需要淘汰一些项 + if c.Options.Size > 0 && c.Options.Size < len(c.Items) { + // 计算需要淘汰的项数 + toEvict := len(c.Items) - c.Options.Size + + // 淘汰多余的项 + for i := 0; i < toEvict; i++ { + c.evictItem() + } + } +} diff --git a/cache/compress.go b/cache/compress.go new file mode 100644 index 0000000..2f2c6ad --- /dev/null +++ b/cache/compress.go @@ -0,0 +1,142 @@ +package cache + +import ( + "bytes" + "compress/gzip" + "io" + "strings" +) + +// CompressOptions 压缩选项 +type CompressOptions struct { + Enabled bool // 是否启用压缩 + Level int // 压缩级别 (1-9),1为最快压缩,9为最佳压缩 + MinSizeBytes int // 最小压缩大小,小于此大小的数据不进行压缩 + Ratio float64 // 压缩比阈值,压缩后大小/原始大小,小于此值才保存压缩结果 +} + +// DefaultCompressOptions 默认压缩选项 +var DefaultCompressOptions = CompressOptions{ + Enabled: true, + Level: 6, // 默认压缩级别为6,平衡压缩率和性能 + MinSizeBytes: 100, // 默认最小压缩大小为100字节 + Ratio: 0.9, // 默认压缩比阈值为0.9,即至少要压缩到原始大小的90%以下才保存压缩结果 +} + +// CompressSVG 压缩SVG数据 +// 返回压缩后的数据和是否进行了压缩 +func CompressSVG(svg string, options CompressOptions) ([]byte, bool) { + if !options.Enabled || len(svg) < options.MinSizeBytes { + return []byte(svg), false + } + + // 创建一个bytes.Buffer来存储压缩数据 + var buf bytes.Buffer + + // 创建一个gzip.Writer,设置压缩级别 + writer, err := gzip.NewWriterLevel(&buf, options.Level) + if err != nil { + return []byte(svg), false + } + + // 写入SVG数据 + _, err = writer.Write([]byte(svg)) + if err != nil { + return []byte(svg), false + } + + // 关闭writer,确保所有数据都被写入 + err = writer.Close() + if err != nil { + return []byte(svg), false + } + + // 获取压缩后的数据 + compressed := buf.Bytes() + + // 计算压缩比 + ratio := float64(len(compressed)) / float64(len(svg)) + + // 如果压缩比不理想,返回原始数据 + if ratio >= options.Ratio { + return []byte(svg), false + } + + return compressed, true +} + +// DecompressSVG 解压缩SVG数据 +func DecompressSVG(data []byte, isCompressed bool) (string, error) { + // 如果数据未压缩,直接返回字符串 + if !isCompressed { + return string(data), nil + } + + // 检查数据是否为gzip格式 + if !isGzipped(data) { + return string(data), nil + } + + // 创建一个gzip.Reader + reader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return string(data), err + } + defer reader.Close() + + // 读取解压缩后的数据 + decompressed, err := io.ReadAll(reader) + if err != nil { + return string(data), err + } + + return string(decompressed), nil +} + +// isGzipped 检查数据是否为gzip格式 +func isGzipped(data []byte) bool { + // gzip文件的魔数是0x1f 0x8b + return len(data) > 2 && data[0] == 0x1f && data[1] == 0x8b +} + +// OptimizeSVG 优化SVG字符串,移除不必要的空白和注释 +func OptimizeSVG(svg string) string { + // 移除XML注释 + svg = removeXMLComments(svg) + + // 移除多余的空白 + svg = removeExtraWhitespace(svg) + + return svg +} + +// removeXMLComments 移除XML注释 +func removeXMLComments(svg string) string { + for { + start := strings.Index(svg, "") + start + if end > start { + svg = svg[:start] + svg[end+3:] + } else { + break + } + } + return svg +} + +// removeExtraWhitespace 移除多余的空白 +func removeExtraWhitespace(svg string) string { + // 替换多个空白字符为单个空格 + svg = strings.Join(strings.Fields(svg), " ") + + // 优化常见的SVG标签周围的空白 + svg = strings.ReplaceAll(svg, "> <", "><") + svg = strings.ReplaceAll(svg, " />", "/>") + svg = strings.ReplaceAll(svg, " =", "=") + svg = strings.ReplaceAll(svg, "= ", "=") + + return svg +} diff --git a/cache/monitor.go b/cache/monitor.go new file mode 100644 index 0000000..3d4b288 --- /dev/null +++ b/cache/monitor.go @@ -0,0 +1,214 @@ +package cache + +import ( + "sync" + "time" +) + +// MonitorOptions 缓存监控选项 +type MonitorOptions struct { + Enabled bool // 是否启用监控 + SampleInterval time.Duration // 采样间隔时间 + AdjustInterval time.Duration // 调整间隔时间 + MinSize int // 最小缓存大小 + MaxSize int // 最大缓存大小 + TargetHitRate float64 // 目标命中率 + SizeGrowthFactor float64 // 缓存大小增长因子 + SizeShrinkFactor float64 // 缓存大小收缩因子 + ExpirationFactor float64 // 过期时间调整因子 +} + +// DefaultMonitorOptions 默认监控选项 +var DefaultMonitorOptions = MonitorOptions{ + Enabled: true, + SampleInterval: time.Minute, // 每分钟采样一次 + AdjustInterval: time.Minute * 10, // 每10分钟调整一次 + MinSize: 50, // 最小缓存大小 + MaxSize: 1000, // 最大缓存大小 + TargetHitRate: 0.8, // 目标命中率80% + SizeGrowthFactor: 1.2, // 增长20% + SizeShrinkFactor: 0.8, // 收缩20% + ExpirationFactor: 1.5, // 过期时间调整因子 +} + +// CacheStats 缓存统计信息 +type CacheStats struct { + Size int // 当前缓存大小 + Hits int // 命中次数 + Misses int // 未命中次数 + HitRate float64 // 命中率 + MemoryUsage int64 // 内存使用量(字节) + LastAdjusted time.Time // 最后调整时间 + SamplesCount int // 样本数量 + AvgAccessTime float64 // 平均访问时间(纳秒) +} + +// Monitor 缓存监控器 +type Monitor struct { + options MonitorOptions + cache *PNCache + stats CacheStats + sampleHistory []CacheStats + mutex sync.RWMutex + stopChan chan struct{} + isRunning bool +} + +// NewMonitor 创建一个新的缓存监控器 +func NewMonitor(cache *PNCache, options MonitorOptions) *Monitor { + return &Monitor{ + options: options, + cache: cache, + sampleHistory: make([]CacheStats, 0, 100), // 预分配100个样本的容量 + stopChan: make(chan struct{}), + isRunning: false, + } +} + +// Start 启动监控器 +func (m *Monitor) Start() { + if !m.options.Enabled || m.isRunning { + return + } + + m.mutex.Lock() + m.isRunning = true + m.mutex.Unlock() + + go m.monitorRoutine() +} + +// Stop 停止监控器 +func (m *Monitor) Stop() { + if !m.isRunning { + return + } + + m.mutex.Lock() + m.isRunning = false + m.mutex.Unlock() + + m.stopChan <- struct{}{} +} + +// GetStats 获取当前缓存统计信息 +func (m *Monitor) GetStats() CacheStats { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return m.stats +} + +// monitorRoutine 监控例程 +func (m *Monitor) monitorRoutine() { + sampleTicker := time.NewTicker(m.options.SampleInterval) + adjustTicker := time.NewTicker(m.options.AdjustInterval) + + defer sampleTicker.Stop() + defer adjustTicker.Stop() + + for { + select { + case <-m.stopChan: + return + case <-sampleTicker.C: + m.collectSample() + case <-adjustTicker.C: + m.adjustCache() + } + } +} + +// collectSample 收集缓存样本 +func (m *Monitor) collectSample() { + m.mutex.Lock() + defer m.mutex.Unlock() + + // 获取缓存统计信息 + hits, misses, hitRate := m.cache.Stats() + size := m.cache.Size() + + // 估算内存使用量(简化计算,实际应用中可能需要更精确的方法) + memoryUsage := int64(size * 1024) // 假设每个缓存项平均占用1KB + + // 创建新的统计样本 + newStat := CacheStats{ + Size: size, + Hits: hits, + Misses: misses, + HitRate: hitRate, + MemoryUsage: memoryUsage, + LastAdjusted: time.Now(), + } + + // 添加到历史样本 + m.sampleHistory = append(m.sampleHistory, newStat) + + // 限制历史样本数量,保留最近的100个样本 + if len(m.sampleHistory) > 100 { + m.sampleHistory = m.sampleHistory[len(m.sampleHistory)-100:] + } + + // 更新当前统计信息 + m.stats = newStat + m.stats.SamplesCount = len(m.sampleHistory) +} + +// adjustCache 根据统计信息调整缓存 +func (m *Monitor) adjustCache() { + m.mutex.Lock() + defer m.mutex.Unlock() + + // 如果样本数量不足,不进行调整 + if len(m.sampleHistory) < 5 { + return + } + + // 计算平均命中率 + totalHitRate := 0.0 + for _, stat := range m.sampleHistory { + totalHitRate += stat.HitRate + } + avgHitRate := totalHitRate / float64(len(m.sampleHistory)) + + // 获取当前缓存选项 + cacheOptions := m.cache.GetOptions() + + // 根据命中率调整缓存大小 + newSize := cacheOptions.Size + if avgHitRate < m.options.TargetHitRate { + // 命中率低于目标,增加缓存大小 + newSize = int(float64(newSize) * m.options.SizeGrowthFactor) + // 确保不超过最大大小 + if newSize > m.options.MaxSize { + newSize = m.options.MaxSize + } + } else if avgHitRate > m.options.TargetHitRate+0.1 && m.stats.Size > m.options.MinSize { + // 命中率远高于目标且缓存大小大于最小值,可以适当减小缓存 + newSize = int(float64(newSize) * m.options.SizeShrinkFactor) + // 确保不小于最小大小 + if newSize < m.options.MinSize { + newSize = m.options.MinSize + } + } + + // 根据访问模式调整过期时间 + newExpiration := cacheOptions.Expiration + if avgHitRate < m.options.TargetHitRate { + // 命中率低,增加过期时间 + newExpiration = time.Duration(float64(newExpiration) * m.options.ExpirationFactor) + } else if avgHitRate > m.options.TargetHitRate+0.1 { + // 命中率高,可以适当减少过期时间 + newExpiration = time.Duration(float64(newExpiration) / m.options.ExpirationFactor) + } + + // 应用新的缓存选项 + if newSize != cacheOptions.Size || newExpiration != cacheOptions.Expiration { + cacheOptions.Size = newSize + cacheOptions.Expiration = newExpiration + m.cache.UpdateOptions(cacheOptions) + + // 更新最后调整时间 + m.stats.LastAdjusted = time.Now() + } +} diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 0000000..9ec9b6c --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,67 @@ +package converter + +import ( + "encoding/base64" + "regexp" + "strings" +) + +var ( + colorAttrRegex = regexp.MustCompile(`(?i)(fill|stroke):(?:#none|transparent)(?:;|\s|"|'|$)`) +) + +type Converter interface { + ToBase64() (string, error) + ToPNG() ([]byte, error) + ToJPEG() ([]byte, error) +} + +type SVGConverter struct { + svgData []byte + width int + height int +} + +func NewSVGConverter(svgData []byte, width, height int) *SVGConverter { + processed := preprocessSVG(svgData) + return &SVGConverter{ + svgData: processed, + width: width, + height: height, + } +} + +func preprocessSVG(data []byte) []byte { + // 1. 移除动画元素 + data = regexp.MustCompile(`]*>`).ReplaceAll(data, []byte{}) + + // 2. 替换 fill:#none, fill:transparent 为 fill:#000000 + processed := colorAttrRegex.ReplaceAllStringFunc(string(data), func(match string) string { + if strings.HasPrefix(strings.ToLower(match), "fill:") { + return "fill:#000;" + } + return match + }) + return []byte(processed) +} + +// ToBase64 returns the SVG data as a base64-encoded string. +func (c *SVGConverter) ToBase64() (string, error) { + return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString(c.svgData), nil +} + +// ToPNG returns the SVG data as a PNG image. +// Note: This is not implemented yet. +// Deprecated: It can't be perfectly implemented for the time being, so it's better to Use ToBase64 instead. +func (c *SVGConverter) ToPNG() ([]byte, error) { + // TODO: implement + return nil, nil +} + +// ToJPEG returns the SVG data as a JPEG image. +// Note: This is not implemented yet. +// Deprecated: It can't be perfectly implemented for the time being, so it's better to Use ToBase64 instead. +func (c *SVGConverter) ToJPEG() ([]byte, error) { + // TODO: implement + return nil, nil +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..fbea02e --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,15 @@ +package errors + +import "errors" + +// 定义错误常量 +var ( + ErrAvatarIDRequired = errors.New("pixelnebula: avatar id is required") + ErrInvalidTheme = errors.New("pixelnebula: invalid theme index") + ErrInvalidPart = errors.New("pixelnebula: invalid part index") + ErrInvalidShapeSetIndex = errors.New("pixelnebula: invalid shape set index") + ErrInvalidShapeType = errors.New("pixelnebula: invalid shape type") + ErrInvalidColor = errors.New("pixelnebula: invalid color scheme") + ErrInsufficientHash = errors.New("pixelnebula: insufficient hash digits generated") + ErrInvalidStyleName = errors.New("pixelnebula: invalid style name") +) diff --git a/examples/01_basic_usage.go b/examples/01_basic_usage.go new file mode 100644 index 0000000..54ce966 --- /dev/null +++ b/examples/01_basic_usage.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// 基本用法示例 +// 展示如何创建简单的PixelNebula头像 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula() + + // 设置风格 - 这里使用默认的AfrohairStyle风格 + pn.WithStyle(style.AfrohairStyle) + + // 设置主题索引 - 每种风格有多个主题可选 + pn.WithTheme(0) + + // 设置头像尺寸 (宽度, 高度) + pn.WithSize(300, 300) + + // 生成SVG - 需要提供唯一ID和是否生成无环境模式的参数 + // 第一个参数:唯一标识符,用于生成不同的头像 + // 第二个参数:是否为无环境模式,true表示不生成背景环境 + svg, err := pn.Generate("my-unique-id-123", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + os.Exit(1) + } + + // 保存到文件 + err = os.WriteFile("basic_avatar.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + os.Exit(1) + } + + fmt.Println("成功生成基本头像: basic_avatar.svg") + + // 再生成一个无环境模式的头像 + svgNoEnv, err := pn.Generate("my-unique-id-123", true).ToSVG() + if err != nil { + fmt.Printf("生成无环境SVG失败: %v\n", err) + os.Exit(1) + } + + // 保存到文件 + err = os.WriteFile("basic_avatar_no_env.svg", []byte(svgNoEnv), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + os.Exit(1) + } + + fmt.Println("成功生成无环境头像: basic_avatar_no_env.svg") +} diff --git a/examples/02_styles_and_themes.go b/examples/02_styles_and_themes.go new file mode 100644 index 0000000..7f47908 --- /dev/null +++ b/examples/02_styles_and_themes.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "os" + "strconv" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// 风格和主题示例 +// 展示如何使用不同的风格和主题生成多个头像 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula() + + // 定义要展示的风格数组 + styles := []style.StyleType{ + style.AteamStyle, + style.GirlStyle, + style.CountryStyle, + style.GeeknotStyle, + style.PunkStyle, + // 可以添加更多内置风格 + } + + // 为每种风格生成不同主题的头像 + for styleIndex, styleType := range styles { + // 设置当前风格 + pn.WithStyle(styleType) + + // 获取风格名称用于文件命名 + var styleName string + switch styleType { + case style.AteamStyle: + styleName = "ateam" + case style.GirlStyle: + styleName = "girl" + case style.CountryStyle: + styleName = "country" + case style.GeeknotStyle: + styleName = "geeknot" + case style.PunkStyle: + styleName = "punk" + default: + styleName = "unknown" + } + + // 对每种风格,生成3个不同主题的头像 + for themeIndex := 0; themeIndex < 3; themeIndex++ { + // 设置主题 + pn.WithTheme(themeIndex) + + // 设置尺寸 + pn.WithSize(200, 200) + + // 生成唯一ID - 这里使用风格和主题索引组合 + uniqueID := "style-" + strconv.Itoa(styleIndex) + "-theme-" + strconv.Itoa(themeIndex) + + // 生成SVG + svg, err := pn.Generate(uniqueID, false).ToSVG() + if err != nil { + fmt.Printf("生成风格%s主题%d的SVG失败: %v\n", styleName, themeIndex, err) + continue + } + + // 文件名 + filename := fmt.Sprintf("%s_theme_%d.svg", styleName, themeIndex) + + // 保存到文件 + err = os.WriteFile(filename, []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件%s失败: %v\n", filename, err) + continue + } + + fmt.Printf("成功生成头像: %s\n", filename) + } + } + + fmt.Println("所有风格和主题头像生成完成!") +} diff --git a/examples/03_custom_theme_and_style.go b/examples/03_custom_theme_and_style.go new file mode 100644 index 0000000..aacad50 --- /dev/null +++ b/examples/03_custom_theme_and_style.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" + "github.com/landaiqing/go-pixelnebula/theme" +) + +// 自定义主题和风格示例 +// 展示如何创建自定义主题和风格 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula() + + // 1. 自定义主题示例 + // 创建自定义主题 - 每个主题包含各部分的颜色设置 + customThemes := []theme.Theme{ + { + theme.ThemePart{ + // 环境部分颜色 + "env": []string{"#FF5733", "#C70039"}, + // 头部颜色 + "head": []string{"#FFC300", "#FF5733"}, + // 衣服颜色 + "clo": []string{"#2E86C1", "#1A5276"}, + // 眼睛颜色 + "eyes": []string{"#000000", "#FFFFFF"}, + // 嘴巴颜色 + "mouth": []string{"#E74C3C"}, + // 头顶装饰颜色 + "top": []string{"#884EA0", "#7D3C98"}, + }, + theme.ThemePart{ + // 另一个主题配色 + "env": []string{"#3498DB", "#2874A6"}, + "head": []string{"#F5CBA7", "#F0B27A"}, + "clo": []string{"#27AE60", "#196F3D"}, + "eyes": []string{"#2C3E50", "#FDFEFE"}, + "mouth": []string{"#CB4335"}, + "top": []string{"#D35400", "#BA4A00"}, + }, + }, + } + + // 应用自定义主题 + pn.WithCustomizeTheme(customThemes) + + // 生成使用自定义主题的头像 + pn.WithSize(250, 250) + pn.WithTheme(0) + + // 生成第一个自定义主题的头像 + svg1, err := pn.Generate("custom-theme-1", false).SetTheme(0).ToSVG() + if err != nil { + fmt.Printf("生成自定义主题1的SVG失败: %v\n", err) + } else { + // 保存到文件 + err = os.WriteFile("custom_theme_1.svg", []byte(svg1), 0644) + if err != nil { + fmt.Printf("保存自定义主题1文件失败: %v\n", err) + } else { + fmt.Println("成功生成自定义主题1头像: custom_theme_1.svg") + } + } + + // 生成第二个自定义主题的头像 + svg2, err := pn.Generate("custom-theme-2", false).SetTheme(1).ToSVG() + if err != nil { + fmt.Printf("生成自定义主题2的SVG失败: %v\n", err) + } else { + // 保存到文件 + err = os.WriteFile("custom_theme_2.svg", []byte(svg2), 0644) + if err != nil { + fmt.Printf("保存自定义主题2文件失败: %v\n", err) + } else { + fmt.Println("成功生成自定义主题2头像: custom_theme_2.svg") + } + } + + // 2. 自定义风格示例 + // 创建一个新的PixelNebula实例,用于自定义风格 + pn2 := pixelnebula.NewPixelNebula() + + // 创建自定义风格 - 每种风格包含不同形状部件的SVG路径 + // 注意:这里仅作示例,实际使用中需要提供完整的SVG路径数据 + customStyles := []style.StyleSet{ + { + // 第一种自定义风格 + style.TypeEnv: ``, + style.TypeHead: ``, + style.TypeClo: ``, + style.TypeEyes: ``, + style.TypeMouth: ``, + style.TypeTop: ``, + }, + } + + // 应用自定义风格 + pn2.WithCustomizeStyle(customStyles) + pn2.WithSize(250, 250) + + // 使用自定义风格生成头像 + svg3, err := pn2.Generate("custom-style", false).SetStyleByIndex(0).ToSVG() + if err != nil { + fmt.Printf("生成自定义风格的SVG失败: %v\n", err) + } else { + // 保存到文件 + err = os.WriteFile("custom_style.svg", []byte(svg3), 0644) + if err != nil { + fmt.Printf("保存自定义风格文件失败: %v\n", err) + } else { + fmt.Println("成功生成自定义风格头像: custom_style.svg") + } + } + + fmt.Println("自定义主题和风格示例完成!") +} diff --git a/examples/04_all_animations.go b/examples/04_all_animations.go new file mode 100644 index 0000000..3001718 --- /dev/null +++ b/examples/04_all_animations.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// 所有动画效果示例 +// 展示PixelNebula支持的所有动画类型 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.AfrohairStyle) + pn.WithTheme(0) + pn.WithSize(300, 300) + + // 1. 旋转动画 - 让环境和头部旋转 + pn.WithRotateAnimation("env", 0, 360, 10, -1) // 无限循环旋转环境 + pn.WithRotateAnimation("head", 0, 360, 15, -1) // 无限循环旋转头部 + + // 2. 渐变动画 - 给环境添加渐变色 + pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + + // 3. 淡入淡出动画 - 让眼睛闪烁 + pn.WithFadeAnimation("eyes", "1", "0.3", 2, -1) + + // 4. 变换动画 - 让嘴巴缩放 + pn.WithTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1) + + // 5. 颜色变换动画 - 让头顶装饰变色 + pn.WithColorAnimation("top", "fill", "#9b59b6", "#e74c3c", 3, -1) + + // 6. 弹跳动画 - 让整个头像上下弹跳 + pn.WithBounceAnimation("head", "translateY", "0", "-10", 3, 5, -1) + + // 7. 波浪动画 - 让衣服产生波浪效果 + pn.WithWaveAnimation("clo", 5, 0.2, "horizontal", 4, -1) + + // 8. 闪烁动画 - 让头顶装饰闪烁 + pn.WithBlinkAnimation("top", 0.3, 1.0, 4, 6, -1) + + // 9. 路径动画 - 让眼睛沿着路径移动 + pn.WithPathAnimation("eyes", "M 0,0 C 10,-10 -10,-10 0,0", 3, -1) + + // 10. 带旋转的路径动画 - 让眼睛在移动的同时旋转 + pn.WithPathAnimationRotate("mouth", "M 0,0 C 5,5 -5,5 0,0", "auto", 4, -1) + + // 生成SVG + svg, err := pn.Generate("all-animations-example", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + os.Exit(1) + } + + // 保存到文件 + err = os.WriteFile("all_animations.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + os.Exit(1) + } + + fmt.Println("成功生成包含所有动画效果的头像: all_animations.svg") +} diff --git a/examples/05_svg_builder_chain.go b/examples/05_svg_builder_chain.go new file mode 100644 index 0000000..df167bd --- /dev/null +++ b/examples/05_svg_builder_chain.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// SVG构建器链式调用示例 +// 展示如何使用链式调用API创建头像 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula().WithDefaultCache() + + // 示例1: 基本链式调用 + // 使用链式调用创建并保存头像 + svg1, err := pn.Generate("chain-example-1", false). + SetStyle(style.AfrohairStyle). + SetTheme(0). + SetSize(200, 200). + ToSVG() + + if err != nil { + fmt.Printf("生成基本链式调用SVG失败: %v\n", err) + } else { + // 保存到文件 + err = os.WriteFile("basic_chain.svg", []byte(svg1), 0644) + if err != nil { + fmt.Printf("保存基本链式调用SVG文件失败: %v\n", err) + } else { + fmt.Println("成功生成基本链式调用头像: basic_chain.svg") + } + } + + // 示例2: 带动画的链式调用 + // 使用链式调用添加多种动画效果 + svg2, err := pn.Generate("chain-example-2", false). + SetStyle(style.GirlStyle). + SetTheme(1). + SetSize(300, 300). + // 添加旋转动画 + SetRotateAnimation("env", 0, 360, 10, -1). + // 添加淡入淡出动画 + SetFadeAnimation("eyes", "1", "0.3", 2, -1). + // 添加变换动画 + SetTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1). + // 添加颜色变换动画 + SetColorAnimation("top", "fill", "#9b59b6", "#e74c3c", 3, -1). + // 构建并获取SVG + ToSVG() + + if err != nil { + fmt.Printf("生成带动画的链式调用SVG失败: %v\n", err) + } else { + // 保存到文件 + err = os.WriteFile("animated_chain.svg", []byte(svg2), 0644) + if err != nil { + fmt.Printf("保存带动画的链式调用SVG文件失败: %v\n", err) + } else { + fmt.Println("成功生成带动画的链式调用头像: animated_chain.svg") + } + } + + // 示例3: 直接保存到文件的链式调用 + err = pn.Generate("chain-example-3", false). + SetStyle(style.BlondStyle). + SetTheme(2). + SetSize(250, 250). + // 添加波浪动画 + SetWaveAnimation("clo", 5, 0.2, "horizontal", 4, -1). + // 添加闪烁动画 + SetBlinkAnimation("top", 0.3, 1.0, 4, 6, -1). + // 构建并直接保存到文件 + Build(). + ToFile("direct_file_chain.svg") + + if err != nil { + fmt.Printf("直接保存到文件的链式调用失败: %v\n", err) + } else { + fmt.Println("成功生成并直接保存头像到文件: direct_file_chain.svg") + } + + // 示例4: 转换为Base64的链式调用 + base64, err := pn.Generate("chain-example-4", false). + SetStyle(style.BlondStyle). + SetTheme(0). + SetSize(200, 200). + // 添加旋转动画 + SetRotateAnimation("head", 0, 360, 15, -1). + // 构建并转换为Base64 + ToBase64() + + if err != nil { + fmt.Printf("转换为Base64的链式调用失败: %v\n", err) + } else { + // 保存Base64编码到文件 + err = os.WriteFile("base64_avatar.txt", []byte(base64), 0644) + if err != nil { + fmt.Printf("保存Base64编码到文件失败: %v\n", err) + } else { + fmt.Println("成功生成Base64编码头像并保存到文件: base64_avatar.txt") + } + } + + fmt.Println("SVG构建器链式调用示例完成!") +} diff --git a/examples/06_cache_system.go b/examples/06_cache_system.go new file mode 100644 index 0000000..888a135 --- /dev/null +++ b/examples/06_cache_system.go @@ -0,0 +1,223 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/cache" + "github.com/landaiqing/go-pixelnebula/style" +) + +// 缓存系统示例 +// 展示如何使用PixelNebula的缓存功能 +func main() { + // 1. 使用默认缓存 + fmt.Println("=== 使用默认缓存示例 ===") + defaultCacheExample() + + // 2. 使用自定义缓存 + fmt.Println("\n=== 使用自定义缓存示例 ===") + customCacheExample() + + // 3. 使用带监控的缓存 + fmt.Println("\n=== 使用带监控的缓存示例 ===") + monitoredCacheExample() + + // 4. 使用压缩缓存 + fmt.Println("\n=== 使用压缩缓存示例 ===") + compressedCacheExample() +} + +// 使用默认缓存示例 +func defaultCacheExample() { + // 创建一个带默认缓存的PixelNebula实例 + pn := pixelnebula.NewPixelNebula().WithDefaultCache() + + // 设置基本属性 + pn.WithStyle(style.AfrohairStyle) + pn.WithTheme(0) + pn.WithSize(200, 200) + + // 第一次生成头像 - 会存入缓存 + startTime1 := time.Now() + _, err := pn.Generate("default-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + return + } + duration1 := time.Since(startTime1) + + // 第二次生成相同头像 - 应该从缓存中获取 + startTime2 := time.Now() + svg2, err := pn.Generate("default-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("从缓存生成SVG失败: %v\n", err) + return + } + duration2 := time.Since(startTime2) + + // 保存第二次生成的头像 + err = os.WriteFile("default_cache.svg", []byte(svg2), 0644) + if err != nil { + fmt.Printf("保存缓存生成的SVG文件失败: %v\n", err) + return + } + + fmt.Printf("第一次生成耗时: %v\n", duration1) + fmt.Printf("第二次生成耗时: %v (使用缓存)\n", duration2) + fmt.Printf("性能提升: %.2f倍\n", float64(duration1)/float64(duration2)) + fmt.Println("成功生成带默认缓存的头像: default_cache.svg") +} + +// 使用自定义缓存示例 +func customCacheExample() { + // 创建自定义缓存选项 + customCacheOptions := cache.CacheOptions{ + Enabled: true, + Size: 100, // 最大缓存条目数 + Expiration: 1 * time.Hour, // 缓存有效期 + } + + // 创建一个带自定义缓存的PixelNebula实例 + pn := pixelnebula.NewPixelNebula().WithCache(customCacheOptions) + + // 设置基本属性 + pn.WithStyle(style.GirlStyle) + pn.WithTheme(1) + pn.WithSize(200, 200) + + // 第一次生成头像 - 会存入缓存 + startTime1 := time.Now() + _, err := pn.Generate("custom-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + return + } + duration1 := time.Since(startTime1) + + // 第二次生成相同头像 - 应该从缓存中获取 + startTime2 := time.Now() + svg2, err := pn.Generate("custom-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("从缓存生成SVG失败: %v\n", err) + return + } + duration2 := time.Since(startTime2) + + // 保存第二次生成的头像 + err = os.WriteFile("custom_cache.svg", []byte(svg2), 0644) + if err != nil { + fmt.Printf("保存缓存生成的SVG文件失败: %v\n", err) + return + } + + fmt.Printf("第一次生成耗时: %v\n", duration1) + fmt.Printf("第二次生成耗时: %v (使用缓存)\n", duration2) + fmt.Printf("性能提升: %.2f倍\n", float64(duration1)/float64(duration2)) + fmt.Println("成功生成带自定义缓存的头像: custom_cache.svg") +} + +// 使用带监控的缓存示例 +func monitoredCacheExample() { + // 创建带监控的缓存选项 + monitorOptions := cache.MonitorOptions{ + Enabled: true, + SampleInterval: 5 * time.Second, + } + + // 创建一个带默认缓存和监控的PixelNebula实例 + pn := pixelnebula.NewPixelNebula().WithDefaultCache().WithMonitoring(monitorOptions) + + // 设置基本属性 + pn.WithStyle(style.AsianStyle) + pn.WithTheme(0) + pn.WithSize(200, 200) + + // 生成多个头像以展示监控效果 + for i := 0; i < 5; i++ { + uniqueID := fmt.Sprintf("monitor-example-%d", i) + svg, err := pn.Generate(uniqueID, false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + continue + } + + // 重复生成相同头像以测试缓存命中 + for j := 0; j < 3; j++ { + _, err = pn.Generate(uniqueID, false).ToSVG() + if err != nil { + fmt.Printf("从缓存生成SVG失败: %v\n", err) + } + } + + // 保存最后一个头像 + if i == 4 { + err = os.WriteFile("monitored_cache.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存带监控缓存的SVG文件失败: %v\n", err) + } else { + fmt.Println("成功生成带监控缓存的头像: monitored_cache.svg") + } + } + } + + // 等待监控报告生成 + fmt.Println("等待监控报告生成...") + time.Sleep(6 * time.Second) +} + +// 使用压缩缓存示例 +func compressedCacheExample() { + // 创建压缩选项 + compressOptions := cache.CompressOptions{ + Enabled: true, + Level: 6, + MinSizeBytes: 100, // 最小压缩大小 (字节) + } + + // 创建一个带默认缓存和压缩的PixelNebula实例 + pn := pixelnebula.NewPixelNebula().WithDefaultCache().WithCompression(compressOptions) + + // 设置基本属性 + pn.WithStyle(style.AfrohairStyle) + pn.WithTheme(0) + pn.WithSize(300, 300) + + // 添加一些动画以增加SVG大小 + pn.WithRotateAnimation("env", 0, 360, 10, -1) + pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + pn.WithFadeAnimation("eyes", "1", "0.3", 2, -1) + pn.WithTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1) + + // 第一次生成头像 - 会存入压缩缓存 + startTime1 := time.Now() + _, err := pn.Generate("compress-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + return + } + duration1 := time.Since(startTime1) + + // 第二次生成相同头像 - 应该从压缩缓存中获取 + startTime2 := time.Now() + svg2, err := pn.Generate("compress-cache-example", false).ToSVG() + if err != nil { + fmt.Printf("从压缩缓存生成SVG失败: %v\n", err) + return + } + duration2 := time.Since(startTime2) + + // 保存第二次生成的头像 + err = os.WriteFile("compressed_cache.svg", []byte(svg2), 0644) + if err != nil { + fmt.Printf("保存压缩缓存的SVG文件失败: %v\n", err) + return + } + + fmt.Printf("第一次生成耗时: %v\n", duration1) + fmt.Printf("第二次生成耗时: %v (使用压缩缓存)\n", duration2) + fmt.Printf("性能提升: %.2f倍\n", float64(duration1)/float64(duration2)) + fmt.Println("成功生成带压缩缓存的头像: compressed_cache.svg") +} diff --git a/examples/07_format_conversion.go b/examples/07_format_conversion.go new file mode 100644 index 0000000..bcdefc2 --- /dev/null +++ b/examples/07_format_conversion.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/go-pixelnebula" + "github.com/landaiqing/go-pixelnebula/style" +) + +// 格式转换示例 +// 展示如何将SVG转换为其他格式 +func main() { + // 创建一个新的PixelNebula实例 + pn := pixelnebula.NewPixelNebula() + + // 设置基本属性 + pn.WithStyle(style.AfrohairStyle) + pn.WithTheme(0) + pn.WithSize(500, 500) // 使用较大尺寸以便转换后的图像清晰 + + // 添加一些动画效果 + pn.WithRotateAnimation("env", 0, 360, 10, -1) + pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f"}, 5, -1, true) + + // 1. 生成并保存SVG文件 + svgData, err := pn.Generate("format-conversion", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + os.Exit(1) + } + + // 保存SVG文件 + err = os.WriteFile("format_conversion.svg", []byte(svgData), 0644) + if err != nil { + fmt.Printf("保存SVG文件失败: %v\n", err) + } else { + fmt.Println("成功生成SVG文件: format_conversion.svg") + } + + // 2. 转换为Base64格式 + base64Data, err := pn.Generate("format-conversion", false).ToBase64() + if err != nil { + fmt.Printf("转换为Base64失败: %v\n", err) + } else { + // 保存Base64数据到文件 + err = os.WriteFile("format_conversion.base64.txt", []byte(base64Data), 0644) + if err != nil { + fmt.Printf("保存Base64文件失败: %v\n", err) + } else { + fmt.Println("成功生成Base64编码文件: format_conversion.base64.txt") + } + } + fmt.Println("格式转换示例完成!") +} diff --git a/examples/08_random_avatar_generator.go b/examples/08_random_avatar_generator.go new file mode 100644 index 0000000..3ef2436 --- /dev/null +++ b/examples/08_random_avatar_generator.go @@ -0,0 +1,33 @@ +// 08_random_avatar_generator.go + +package main + +import ( + "github.com/landaiqing/go-pixelnebula" + "math/rand" + "os" + "strconv" + "time" +) + +const ( + savePath = "random_generated_avatars.svg" +) + +// 随机生成头像 +// Note: 随机传入id即可随机生成头像,固定的id会生成相同的头像 +func main() { + + // 创建随机数 + rand.Seed(time.Now().UnixNano()) + randomInt := rand.Intn(100) + + pixelNebula := pixelnebula.NewPixelNebula().WithDefaultCache() + svg, err := pixelNebula.Generate(strconv.Itoa(randomInt), false).ToSVG() + if err != nil { + panic(err) + } + // 保存图片 + os.WriteFile(savePath, []byte(svg), 0644) + defer os.Remove(savePath) +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..02003e9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,117 @@ +# PixelNebula 示例 + +[英文版](README_EN.md) | 中文版 + +## 示例文件 + +这个目录包含了多个示例,展示了如何使用 PixelNebula 库的各种功能。每个示例都是独立的,你可以单独运行它们来了解特定的功能。 + +| 文件名 | 描述 | +|--------|------| +| [01_basic_usage.go](01_basic_usage.go) | 演示基本的头像生成,包括常规头像和无环境头像 | +| [02_styles_and_themes.go](02_styles_and_themes.go) | 展示如何使用不同的样式和主题 | +| [03_custom_theme_and_style.go](03_custom_theme_and_style.go) | 演示如何创建和使用自定义主题和样式 | +| [04_all_animations.go](04_all_animations.go) | 展示所有支持的动画效果 | +| [05_svg_builder_chain.go](05_svg_builder_chain.go) | 演示如何使用链式API生成SVG | +| [06_cache_system.go](06_cache_system.go) | 展示缓存系统的功能,包括默认缓存、自定义缓存和监控 | +| [07_format_conversion.go](07_format_conversion.go) | 展示如何将SVG转换为其他格式 | +| [08_random_avatar_generator.go](08_random_avatar_generator.go) | 交互式的随机头像生成器,支持多种样式、主题和输出格式 | + +## 如何运行示例 + +确保您已正确安装 Go 环境并设置了 GOPATH。然后,按照以下步骤运行示例: + +### 运行单个示例 + +```bash +# 例如,运行基本用法示例 +go run 01_basic_usage.go +``` + +### 运行所有示例 + +```bash +for file in *_*.go; do + echo "🚀 运行示例: $file" + go run $file + echo "------------------------" +done +``` + +## 示例说明 + +### 01_basic_usage.go + +这个示例展示了 PixelNebula 的基本功能,包括: + +- 创建一个基本的头像 +- 生成一个无环境的头像 +- 处理错误和保存文件 + +### 02_styles_and_themes.go + +这个示例展示了如何使用不同的样式和主题: + +- 使用预定义的样式生成头像 +- 应用不同的主题 +- 组合样式和主题 + +### 03_custom_theme_and_style.go + +这个示例展示了如何创建和使用自定义主题和样式: + +- 创建自定义颜色主题 +- 定义自定义样式 +- 组合自定义主题和样式 + +### 04_all_animations.go + +这个示例展示了所有支持的动画效果: + +- 旋转动画 +- 渐变动画 +- 淡入淡出效果 +- 变换动画 +- 颜色变换 +- 弹跳效果 +- 波浪动画 +- 闪烁效果 +- 路径动画 + +### 05_svg_builder_chain.go + +这个示例展示了如何使用链式API: + +- 使用链式调用创建简单的SVG +- 添加动画效果 +- 直接保存到文件 +- 转换为Base64 + +### 06_cache_system.go + +这个示例展示了缓存系统的功能: + +- 使用默认缓存 +- 配置自定义缓存 +- 监控缓存性能 +- 使用压缩缓存 + +### 07_format_conversion.go + +这个示例展示了格式转换功能: + +- 转换为Base64 +- 其他格式暂未找到完美解决方案,欢迎 PR + +### 08_random_avatar_generator.go + +这个示例是一个交互式的随机头像生成器: + +- 随机生成不同样式和主题的头像 + +## 提示 + +- 每个示例文件顶部都有详细的注释,解释了该示例所展示的功能 +- 如果遇到任何问题,请检查文件中的错误处理部分 +- 生成的头像会保存在示例代码指定的位置 +- 有些示例可能需要创建目录来保存生成的文件 \ No newline at end of file diff --git a/examples/README_EN.md b/examples/README_EN.md new file mode 100644 index 0000000..d4622d7 --- /dev/null +++ b/examples/README_EN.md @@ -0,0 +1,117 @@ +# PixelNebula Examples + +[中文版](README.md) | English + +## Example Files + +This directory contains multiple examples showcasing various features of the PixelNebula library. Each example is standalone and can be run separately to understand specific functionalities. + +| Filename | Description | +|----------|-------------| +| [01_basic_usage.go](01_basic_usage.go) | Demonstrates basic avatar generation, including regular and no-environment avatars | +| [02_styles_and_themes.go](02_styles_and_themes.go) | Shows how to use different styles and themes | +| [03_custom_theme_and_style.go](03_custom_theme_and_style.go) | Demonstrates how to create and use custom themes and styles | +| [04_all_animations.go](04_all_animations.go) | Showcases all supported animation effects | +| [05_svg_builder_chain.go](05_svg_builder_chain.go) | Demonstrates how to use the chainable API to generate SVGs | +| [06_cache_system.go](06_cache_system.go) | Shows the cache system functionality, including default, custom, and monitored caching | +| [07_format_conversion.go](07_format_conversion.go) | Shows how to convert SVGs to other formats | +| [08_random_avatar_generator.go](08_random_avatar_generator.go) | Interactive random avatar generator with support for multiple styles, themes, and output formats | + +## How to Run Examples + +Ensure you have Go properly installed and GOPATH set correctly. Then, follow these steps to run the examples: + +### Run a Single Example + +```bash +# For example, run the basic usage example +go run 01_basic_usage.go +``` + +### Run All Examples + +```bash +for file in *_*.go; do + echo "🚀 Running example: $file" + go run $file + echo "------------------------" +done +``` + +## Example Details + +### 01_basic_usage.go + +This example demonstrates the basic functionality of PixelNebula, including: + +- Creating a basic avatar +- Generating a no-environment avatar +- Handling errors and saving files + +### 02_styles_and_themes.go + +This example shows how to use different styles and themes: + +- Using predefined styles to generate avatars +- Applying different themes +- Combining styles and themes + +### 03_custom_theme_and_style.go + +This example shows how to create and use custom themes and styles: + +- Creating custom color themes +- Defining custom styles +- Combining custom themes and styles + +### 04_all_animations.go + +This example showcases all supported animation effects: + +- Rotation animation +- Gradient animation +- Fade-in/out effects +- Transform animation +- Color transformation +- Bounce effects +- Wave animation +- Blink effects +- Path animation + +### 05_svg_builder_chain.go + +This example shows how to use the chainable API: + +- Using chain calls to create simple SVGs +- Adding animation effects +- Saving directly to file +- Converting to Base64 + +### 06_cache_system.go + +This example demonstrates the cache system functionality: + +- Using the default cache +- Configuring custom caches +- Monitoring cache performance +- Using compressed caching + +### 07_format_conversion.go + +This example shows the format conversion capabilities: + +- Converting to Base64 +- We haven't found a perfect solution for other formats yet, please feel free to make a PR + +### 08_random_avatar_generator.go + +This example is an interactive random avatar generator: + +- Randomly generating avatars with different styles and themes + +## Tips + +- Each example file has detailed comments at the top explaining the functionality it demonstrates +- Check the error handling sections in the files if you encounter any issues +- Generated avatars will be saved in the locations specified in the example code +- Some examples may require creating directories to save generated files \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8f8802e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/landaiqing/go-pixelnebula + +go 1.24.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/pixelnebula.go b/pixelnebula.go new file mode 100644 index 0000000..7797c5e --- /dev/null +++ b/pixelnebula.go @@ -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: "", + 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("", 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, ""); end != -1 { + rotateAnimationSVGs[anim.GetTargetID()] = svgCode[start : start+end+2] + } + } + } + + } + + // 处理元素,如果元素有旋转动画,则包裹在g标签中并添加animateTransform + // 只有当不是无环境模式时才添加环境 + if !sansEnv { + if _, hasRotate := rotateAnimations["env"]; hasRotate { + builder.WriteString("\n") + builder.WriteString(final["env"]) + // 添加animateTransform标签 + if animSVG, ok := rotateAnimationSVGs["env"]; ok { + builder.WriteString(animSVG) + } + builder.WriteString("\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("\n") + builder.WriteString(final[elem]) + + // 添加animateTransform标签 + if animSVG, ok := rotateAnimationSVGs[elem]; ok { + // 提取animateTransform标签部分 + if start := strings.Index(animSVG, ""); end != -1 { + builder.WriteString(animSVG[start : start+end+2]) + } + } + } + builder.WriteString("\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 +} diff --git a/pixelnebula_test.go b/pixelnebula_test.go new file mode 100644 index 0000000..190548c --- /dev/null +++ b/pixelnebula_test.go @@ -0,0 +1,236 @@ +package pixelnebula + +import ( + "encoding/hex" + "fmt" + "github.com/landaiqing/go-pixelnebula/style" + "os" + "regexp" + "testing" +) + +func TestPixelNebula(t *testing.T) { + pn := NewPixelNebula() + numRegex := regexp.MustCompile(`[0-9]`) + + // 测试多个不同的ID + testIDs := []string{ + "example_avatar0", + "example_avatar1", + "example_avatar2", + "example_avatar3", + "example_avatar4", + } + + // 打印可用的风格和主题数量 + fmt.Printf("总风格数量: %d\n", pn.themeManager.StyleCount()) + for i := 0; i < pn.themeManager.StyleCount(); i++ { + fmt.Printf("风格 %d 的主题数量: %d\n", i, pn.themeManager.ThemeCount(i)) + } + + for i, id := range testIDs { + // 生成并保存头像 + builder := pn.Generate(id, false) + svg, err := builder.ToSVG() + if err != nil { + t.Errorf("生成头像失败 (ID: %s): %v", id, err) + continue + } + + // 保存每个头像到不同的文件 + filename := fmt.Sprintf("avatar_%d.svg", i) + err = os.WriteFile(filename, []byte(svg), 0644) + if err != nil { + t.Errorf("保存头像失败 (ID: %s): %v", id, err) + continue + } + + // 打印调试信息 + pn.hasher.Reset() + pn.hasher.Write([]byte(id)) + sum := pn.hasher.Sum(nil) + hashStr := hex.EncodeToString(sum) + + // 提取数字 + numbers := numRegex.FindAllString(hashStr, -1) + hashNum := pn.hashToNum(numbers) + + fmt.Printf("\nID: %s\n", id) + fmt.Printf("Hash: %s\n", hashStr) + fmt.Printf("Numbers: %v\n", numbers) + fmt.Printf("HashNum: %d\n", hashNum) + + // 计算并打印每个部分的索引 + parts := []string{"env", "clo", "head", "mouth", "eyes", "top"} + for j, part := range parts { + start := j * 2 + end := start + 2 + if end > len(numbers) { + end = len(numbers) + } + partHash := numbers[start:end] + key := pn.calcKey(partHash, nil) + fmt.Printf("%s - StyleIndex: %d, ThemeIndex: %d\n", part, key[0], key[1]) + } + fmt.Printf("------------------\n") + } +} + +func TestAnimation(t *testing.T) { + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.AfrohairStyle) + pn.WithTheme(0) + + // 1. 旋转动画 - 让环境和头部旋转 + pn.WithRotateAnimation("env", 0, 360, 10, -1) // 无限循环旋转环境 + + // 2. 渐变动画 - 让环境渐变 + pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + // 2. 渐变动画 - 让眼睛渐变 + pn.WithGradientAnimation("eyes", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + + // 3. 淡入淡出动画 - 让眼睛闪烁 + pn.WithFadeAnimation("eyes", "1", "0.3", 2, -1) + + // 4. 变换动画 - 让嘴巴缩放 + //pn.WithTransformAnimation("mouth", "scale", "1 1", "1.2 1.2", 1, -1) + + // 5. 颜色变换动画 - 让头发颜色变换 + pn.WithColorAnimation("top", "fill", "#9b59b6", "#e74c3c", 3, -1) + // 5. 颜色变换动画 - 让衣服颜色变换 + pn.WithColorAnimation("clo", "fill", "#9b59b6", "#e74c3c", 3, -1) + + // 6. 弹跳动画 - 让嘴巴弹跳 + pn.WithBounceAnimation("mouth", "transform", "0,0", "0,-10", 5, 2.5, -1) + // 6. 旋转动画 - 让嘴巴旋转 + pn.WithRotateAnimation("mouth", 0, 360, 10, -1) // 无限循环旋转环境 + + //// 7. 波浪动画 - 让衣服产生波浪效果 + //pn.WithWaveAnimation("clo", 5, 0.2, "horizontal", 4, -1) + + // 8. 闪烁动画 - 让头顶装饰闪烁 + //pn.WithBlinkAnimation("head", 0.3, 1.0, 4, 6, -1) + // 8. 波浪动画 - 让环境产生波浪效果 + //pn.WithWaveAnimation("clo", 5, 2, "horizontal", 4, -1) + + // 9. 路径动画 - 让眼睛沿着路径移动 + //pn.WithPathAnimation("eyes", "M 0,0 C 10,-10 -10,-10 0,0", 3, -1) + + pn.WithBounceAnimation("eyes", "transform", "0,0", "0,-5", 5, 2, -1) + + // 10. 带旋转的路径动画 - 让眼睛在移动的同时旋转 + //pn.WithPathAnimationRotate("mouth", "M 0,0 C 5,5 -5,5 0,0", "auto", 4, -1) + + // 生成SVG + svg, err := pn.Generate("example_avatar", false).ToSVG() + if err != nil { + fmt.Printf("生成SVG失败: %v\n", err) + os.Exit(1) + } + + // 保存到文件 + err = os.WriteFile("./assets/example_avatar.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + os.Exit(1) + } +} + +func TestDemo(t *testing.T) { + + // 创建一个新的 PixelNebula 实例 + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.GirlStyle) + pn.WithSize(231, 231) + + // 生成 SVG 并保存到文件 + svg, err := pn.Generate("unique-id-123", false).ToSVG() + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + return + } + + // 保存到文件 + err = os.WriteFile("my_avatar.svg", []byte(svg), 0644) + if err != nil { + fmt.Printf("保存文件失败: %v\n", err) + return + } + + fmt.Println("头像成功生成: my_avatar.svg") + +} + +func TestRotateAnimation(t *testing.T) { + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.FirehairStyle) + pn.WithTheme(0) + + // 1. 旋转动画 - 让环境和头部旋转 + pn.WithRotateAnimation("eyes", 0, 360, 10, -1) // 无限循环旋转环境 + + err := pn.Generate("example_avatar", false).ToFile("example_avatar.svg") + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + os.Exit(1) + } +} + +func TestGradientAnimation(t *testing.T) { + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.FirehairStyle) + pn.WithTheme(0) + + // 2. 渐变动画 - 让环境渐变 + pn.WithGradientAnimation("env", []string{"#3498db", "#2ecc71", "#f1c40f", "#e74c3c", "#9b59b6"}, 8, -1, true) + + err := pn.Generate("example_avatar", false).ToFile("example_avatar.svg") + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + os.Exit(1) + } +} + +// 测试淡入淡出动画 +func TestFadeAnimation(t *testing.T) { + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.FirehairStyle) + pn.WithTheme(0) + + // 3. 淡入淡出动画 - 让眼睛闪烁 + pn.WithFadeAnimation("head", "1", "0.3", 2, -1) + + err := pn.Generate("example_avatar", false).ToFile("example_avatar.svg") + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + os.Exit(1) + } +} + +// 测试路径动画 +func TestPathAnimation(t *testing.T) { + pn := NewPixelNebula() + + // 设置风格和尺寸 + pn.WithStyle(style.FirehairStyle) + pn.WithTheme(0) + + // 9. 路径动画 - 让clo沿着路径移动 + pn.WithPathAnimation("clo", "M 0,0 C 10,-10 -10,-10 0,0", 3, -1) + + err := pn.Generate("example_avatar", false).ToFile("example_avatar.svg") + if err != nil { + fmt.Printf("生成 SVG 失败: %v\n", err) + os.Exit(1) + } +} diff --git a/style/init.go b/style/init.go new file mode 100644 index 0000000..1388808 --- /dev/null +++ b/style/init.go @@ -0,0 +1,196 @@ +package style + +var defaultStyleSet = map[StyleType]StyleSet{ + // 创建第一组形状集合 - Robo风格 + RoboStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // 创建第二组形状集合 - Girl风格 + GirlStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Blonde 风格 + BlondeStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + // Guy 形状集合 + GuyStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Country 风格 + CountryStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Geeknot 风格 + GeeknotStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Asian 风格 + AsianStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Punk 风格 + PunkStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Afrohair 风格 + AfrohairStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Normie Female 风格 + NormieFemaleStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + //Older 风格 + OlderStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Firehair 风格 + FirehairStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + // Blond 风格 + BlondStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Ateam 风格 + AteamStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + // Rasta 风格 + RastaStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + // Meta 风格 + MetaStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, + + // Square 风格 + SquareStyle: { + TypeClo: "", + TypeMouth: "", + TypeEyes: "", + TypeTop: "", + TypeHead: "", + TypeEnv: "", + }, +} + +// initShapes 初始化形状数据 +func (m *Manager) initShapes() { + for _, style := range []StyleType{ + RoboStyle, + GirlStyle, + BlondeStyle, + GuyStyle, + CountryStyle, + GeeknotStyle, + AsianStyle, + PunkStyle, + AfrohairStyle, + NormieFemaleStyle, + OlderStyle, + FirehairStyle, + BlondStyle, + AteamStyle, + RastaStyle, + MetaStyle, + SquareStyle, + } { + if styleSet, exists := defaultStyleSet[style]; exists { + m.AddStyleSet(styleSet) + } + } +} diff --git a/style/style.go b/style/style.go new file mode 100644 index 0000000..c4dcc95 --- /dev/null +++ b/style/style.go @@ -0,0 +1,115 @@ +package style + +import "github.com/landaiqing/go-pixelnebula/errors" + +// ShapeType 表示形状类型 +type ShapeType string + +// 预定义形状类型 +const ( + TypeClo ShapeType = "clo" // 衣服 + TypeMouth ShapeType = "mouth" // 嘴巴 + TypeEyes ShapeType = "eyes" // 眼睛 + TypeTop ShapeType = "top" // 头顶 + TypeHead ShapeType = "head" // 头部 + TypeEnv ShapeType = "env" // 环境/背景 +) + +// 预定义风格类型常量 +const ( + RoboStyle StyleType = "robo" + GirlStyle StyleType = "girl" + BlondeStyle StyleType = "blonde" + GuyStyle StyleType = "guy" + CountryStyle StyleType = "country" + GeeknotStyle StyleType = "geeknot" + AsianStyle StyleType = "asian" + PunkStyle StyleType = "punk" + AfrohairStyle StyleType = "afrohair" + NormieFemaleStyle StyleType = "normiefemale" + OlderStyle StyleType = "older" + FirehairStyle StyleType = "firehair" + BlondStyle StyleType = "blond" + AteamStyle StyleType = "ateam" + RastaStyle StyleType = "rasta" + MetaStyle StyleType = "meta" + SquareStyle StyleType = "square" +) + +// StyleType 表示风格类型 +type StyleType string + +// StyleSet 表示一组形状 +type StyleSet map[ShapeType]string + +// Manager 形状管理器,负责管理所有形状 +type Manager struct { + styleSets []StyleSet +} + +// NewShapeManager 创建一个新的形状管理器 +func NewShapeManager() *Manager { + m := &Manager{} + m.initShapes() + return m +} + +// GetShape 获取指定索引和类型的形状 +func (m *Manager) GetShape(setIndex int, shapeType ShapeType) (string, error) { + if setIndex < 0 || setIndex >= len(m.styleSets) { + return "", errors.ErrInvalidShapeSetIndex + } + + shapeSet := m.styleSets[setIndex] + shape, ok := shapeSet[shapeType] + if !ok { + return "", errors.ErrInvalidShapeType + } + + return shape, nil +} + +// StyleSetCount 返回形状集合数量 +func (m *Manager) StyleSetCount() int { + return len(m.styleSets) +} + +// AddStyleSet 添加一个新形状集合 +func (m *Manager) AddStyleSet(shapeSet StyleSet) int { + m.styleSets = append(m.styleSets, shapeSet) + return len(m.styleSets) - 1 +} + +// CustomizeStyle 自定义风格 +func (m *Manager) CustomizeStyle(styleSets []StyleSet) { + m.styleSets = styleSets +} + +// GetStyleIndex 根据风格类型获取对应的索引值 +func (m *Manager) GetStyleIndex(style StyleType) (int, error) { + // 遍历已初始化的风格列表获取索引 + for i, s := range []StyleType{ + RoboStyle, + GirlStyle, + BlondeStyle, + GuyStyle, + CountryStyle, + GeeknotStyle, + AsianStyle, + PunkStyle, + AfrohairStyle, + NormieFemaleStyle, + OlderStyle, + FirehairStyle, + BlondStyle, + AteamStyle, + RastaStyle, + MetaStyle, + SquareStyle, + } { + if s == style { + return i, nil + } + } + return -1, errors.ErrInvalidStyleName +} diff --git a/theme/init.go b/theme/init.go new file mode 100644 index 0000000..d6c39b6 --- /dev/null +++ b/theme/init.go @@ -0,0 +1,515 @@ +package theme + +import "github.com/landaiqing/go-pixelnebula/style" + +var defaultThemeSet = map[style.StyleType]Theme{ + style.RoboStyle: { + ThemePart{ + "env": {"ff2f2b"}, + "clo": {"fff", "000"}, + "head": {"fff"}, + "mouth": {"fff", "000", "000"}, + "eyes": {"000", "none", "0ff"}, + "top": {"fff", "fff"}, + }, + // 第二部分 + ThemePart{ + "env": {"ff1ec1"}, + "clo": {"000", "fff"}, + "head": {"ffc1c1"}, + "mouth": {"fff", "000", "000"}, + "eyes": {"FF2D00", "fff", "none"}, + "top": {"a21d00", "fff"}, + }, + // 第三部分 + ThemePart{ + "env": {"0079b1"}, + "clo": {"0e00b1", "d1fffe"}, + "head": {"f5aa77"}, + "mouth": {"fff", "000", "000"}, + "eyes": {"0c00de", "fff", "none"}, + "top": {"acfffd", "acfffd"}, + }, + }, + + // 创建Girl主题 + style.GirlStyle: { + // 第一部分 + ThemePart{ + "env": {"a50000"}, + "clo": {"f06", "8e0039"}, + "head": {"85492C"}, + "mouth": {"000"}, + "eyes": {"000", "ff9809"}, + "top": {"ff9809", "ff9809", "none", "none"}, + }, + // 第二部分 + ThemePart{ + "env": {"40E83B"}, + "clo": {"00650b", "62ce5a"}, + "head": {"f7c1a6"}, + "mouth": {"6e1c1c"}, + "eyes": {"000", "ff833b"}, + "top": {"67FFCC", "none", "none", "ecff3b"}, + }, + // 第三部分 + ThemePart{ + "env": {"ff2c2c"}, + "clo": {"fff", "000"}, + "head": {"ffce8b"}, + "mouth": {"000"}, + "eyes": {"000", "ff9809"}, + "top": {"ff9809", "ff9809", "none", "none"}, + }, + }, + + // 创建Blonde主题 + style.BlondeStyle: { + // 第一部分 + ThemePart{ + "env": {"00aad4"}, + "clo": {"fff", "000"}, + "head": {"ffe0bd"}, + "mouth": {"ff9a84"}, + "eyes": {"000", "fff"}, + "top": {"fff200", "fff200"}, + }, + // 第二部分 + ThemePart{ + "env": {"00aad4"}, + "clo": {"fff", "000"}, + "head": {"ffe0bd"}, + "mouth": {"ff9a84"}, + "eyes": {"000", "fff"}, + "top": {"fff200", "fff200"}, + }, + // 第三部分 + ThemePart{ + "env": {"00aad4"}, + "clo": {"fff", "000"}, + "head": {"ffe0bd"}, + "mouth": {"ff9a84"}, + "eyes": {"000", "fff"}, + "top": {"fff200", "fff200"}, + }, + }, + + // Guy 主题 + style.GuyStyle: { + ThemePart{ + "env": {"#6FC30E"}, + "clo": {"#b4e1fa", "#5b5d6e", "#515262", "#a0d2f0", "#a0d2f0"}, + "head": {"#fae3b9"}, + "mouth": {"#fff", "#000"}, + "eyes": {"#000"}, + "top": {"#8eff45", "#8eff45", "none", "none"}, + }, + ThemePart{ + "env": {"#00a58c"}, + "clo": {"#000", "#5b00", "#5100", "#a000", "#a000"}, + "head": {"#FAD2B9"}, + "mouth": {"#fff", "#000"}, + "eyes": {"#000"}, + "top": {"#FFC600", "none", "#FFC600", "none"}, + }, + ThemePart{ + "env": {"#ff501f"}, + "clo": {"#000", "#ff0000", "#ff0000", "#7d7d7d", "#7d7d7d"}, + "head": {"#fff3dc"}, + "mouth": {"#d2001b", "none"}, + "eyes": {"#000"}, + "top": {"#D2001B", "none", "none", "#D2001B"}, + }, + }, + + // Country主题 + style.CountryStyle: { + ThemePart{ + "env": {"#fc0"}, + "clo": {"#901e0e", "#ffbe1e", "#ffbe1e", "#c55f54"}, + "head": {"#f8d9ad"}, + "mouth": {"#000", "none", "#000", "none"}, + "eyes": {"#000"}, + "top": {"#583D00", "#AF892E", "#462D00", "#a0a0a0"}, + }, + ThemePart{ + "env": {"#386465"}, + "clo": {"#fff", "#333", "#333", "#333"}, + "head": {"#FFD79D"}, + "mouth": {"#000", "#000", "#000", "#000"}, + "eyes": {"#000"}, + "top": {"#27363C", "#5DCAD4", "#314652", "#333"}, + }, + ThemePart{ + "env": {"#DFFF00"}, + "clo": {"#304267", "#aab0b1", "#aab0b1", "#aab0b1"}, + "head": {"#e6b876"}, + "mouth": {"#50230a", "#50230a", "#50230a", "#50230a"}, + "eyes": {"#000"}, + "top": {"#333", "#afafaf", "#222", "#6d3a1d"}, + }, + }, + + // Geeknot主题 + style.GeeknotStyle: { + ThemePart{ + "env": {"#a09300"}, + "clo": {"#c7d4e2", "#435363", "#435363", "#141720", "#141720", "#e7ecf2", "#e7ecf2"}, + "head": {"#f5d4a6"}, + "mouth": {"#000", "#cf9f76"}, + "eyes": {"#000", "#000", "#000", "#000", "#000", "#000", "#fff", "#fff", "#fff", "#fff", "#000", "#000"}, + "top": {"none", "#fdff00"}, + }, + ThemePart{ + "env": {"#b3003e"}, + "clo": {"#000", "#435363", "#435363", "#000", "none", "#e7ecf2", "#e7ecf2"}, + "head": {"#f5d4a6"}, + "mouth": {"#000", "#af9f94"}, + "eyes": {"#9ff3ffdb", "#000", "#9ff3ffdb", "#000", "#2f508a", "#000", "#000", "#000", "none", "none", "none", "none"}, + "top": {"#ff9a00", "#ff9a00"}, + }, + ThemePart{ + "env": {"#884f00"}, + "clo": {"#ff0000", "#fff", "#fff", "#141720", "#141720", "#e7ecf2", "#e7ecf2"}, + "head": {"#c57b14"}, + "mouth": {"#000", "#cf9f76"}, + "eyes": {"none", "#000", "none", "#000", "#5a0000", "#000", "#000", "#000", "none", "none", "none", "none"}, + "top": {"#efefef", "none"}, + }, + }, + + // Asian主题 + style.AsianStyle: { + ThemePart{ + "env": {"#8acf00"}, + "clo": {"#ee2829", "#ff0"}, + "head": {"#ffce73"}, + "mouth": {"#fff", "#000"}, + "eyes": {"#000"}, + "top": {"#000", "#000", "none", "#000", "#ff4e4e", "#000"}, + }, + ThemePart{ + "env": {"#00d2a3"}, + "clo": {"#0D0046", "#ffce73"}, + "head": {"#ffce73"}, + "mouth": {"#000", "none"}, + "eyes": {"#000"}, + "top": {"#000", "#000", "#000", "none", "#ffb358", "#000", "none", "none"}, + }, + ThemePart{ + "env": {"#ff184e"}, + "clo": {"#000", "none"}, + "head": {"#ffce73"}, + "mouth": {"#ff0000", "none"}, + "eyes": {"#000"}, + "top": {"none", "none", "none", "none", "none", "#ffc107", "none", "none"}, + }, + }, + + // Punk主题 + style.PunkStyle: { + ThemePart{ + "env": {"#00deae"}, + "clo": {"#ff0000"}, + "head": {"#ffce94"}, + "mouth": {"#f73b6c", "#000"}, + "eyes": {"#e91e63", "#000", "#e91e63", "#000", "#000", "#000"}, + "top": {"#dd104f", "#dd104f", "#f73b6c", "#dd104f"}, + }, + ThemePart{ + "env": {"#181284"}, + "clo": {"#491f49", "#ff9809", "#491f49"}, + "head": {"#f6ba97"}, + "mouth": {"#ff9809", "#000"}, + "eyes": {"#c4ffe4", "#000", "#c4ffe4", "#000", "#000", "#000"}, + "top": {"none", "none", "#d6f740", "#516303"}, + }, + ThemePart{ + "env": {"#bcf700"}, + "clo": {"#ff14e4", "#000", "#14fffd"}, + "head": {"#7b401e"}, + "mouth": {"#666", "#000"}, + "eyes": {"#00b5b4", "#000", "#00b5b4", "#000", "#000", "#000"}, + "top": {"#14fffd", "#14fffd", "#14fffd", "#0d3a62"}, + }, + }, + + // Afrohair主题 + style.AfrohairStyle: { + ThemePart{ + "env": {"#0df"}, + "clo": {"#571e57", "#ff0"}, + "head": {"#f2c280"}, + "mouth": {"#ff0000"}, + "eyes": {"#795548", "#000"}, + "top": {"#de3b00", "none"}, + }, + ThemePart{ + "env": {"#B400C2"}, + "clo": {"#0D204A", "#00ffdf"}, + "head": {"#ca8628"}, + "mouth": {"#1a1a1a"}, + "eyes": {"#cbbdaf", "#000"}, + "top": {"#000", "#000"}, + }, + ThemePart{ + "env": {"#ffe926"}, + "clo": {"#00d6af", "#000"}, + "head": {"#8c5100"}, + "mouth": {"#7d0000"}, + "eyes": {"none", "#000"}, + "top": {"#f7f7f7", "none"}, + }, + }, + + // Normie female主题 + style.NormieFemaleStyle: { + ThemePart{ + "env": {"#4aff0c"}, + "clo": {"#101010", "#fff", "#fff"}, + "head": {"#dbbc7f"}, + "mouth": {"#000"}, + "eyes": {"#000", "none", "none"}, + "top": {"#531148", "#531148", "#531148", "none"}, + }, + ThemePart{ + "env": {"#FFC107"}, + "clo": {"#033c58", "#fff", "#fff"}, + "head": {"#dbc97f"}, + "mouth": {"#000"}, + "eyes": {"none", "#fff", "#000"}, + "top": {"#FFEB3B", "#FFEB3B", "none", "#FFEB3B"}, + }, + ThemePart{ + "env": {"#FF9800"}, + "clo": {"#b40000", "#fff", "#fff"}, + "head": {"#E2AF6B"}, + "mouth": {"#000"}, + "eyes": {"none", "#fff", "#000"}, + "top": {"#ec0000", "#ec0000", "none", "none"}, + }, + }, + + // Older主题 + style.OlderStyle: { + ThemePart{ + "env": {"#104c8c"}, + "clo": {"#354B65", "#3D8EBB", "#89D0DA", "#00FFFD"}, + "head": {"#cc9a5c"}, + "mouth": {"#222", "#fff"}, + "eyes": {"#000", "#000"}, + "top": {"#fff", "#fff", "none"}, + }, + ThemePart{ + "env": {"#0DC15C"}, + "clo": {"#212121", "#fff", "#212121", "#fff"}, + "head": {"#dca45f"}, + "mouth": {"#111", "#633b1d"}, + "eyes": {"#000", "#000"}, + "top": {"none", "#792B74", "#792B74"}, + }, + ThemePart{ + "env": {"#ffe500"}, + "clo": {"#1e5e80", "#fff", "#1e5e80", "#fff"}, + "head": {"#e8bc86"}, + "mouth": {"#111", "none"}, + "eyes": {"#000", "#000"}, + "top": {"none", "none", "#633b1d"}, + }, + }, + + // Firehair主题 + style.FirehairStyle: { + ThemePart{ + "env": {"#4a3f73"}, + "clo": {"#e6e9ee", "#f1543f", "#ff7058", "#fff", "#fff"}, + "head": {"#b27e5b"}, + "mouth": {"#191919", "#191919"}, + "eyes": {"#000", "#000", "#57FFFD"}, + "top": {"#ffc", "#ffc", "#ffc"}, + }, + ThemePart{ + "env": {"#00a08d"}, + "clo": {"#FFBA32", "#484848", "#4e4e4e", "#fff", "#fff"}, + "head": {"#ab5f2c"}, + "mouth": {"#191919", "#191919"}, + "eyes": {"#000", "#ff23fa63", "#000"}, + "top": {"#ff90f4", "#ff90f4", "#ff90f4"}, + }, + ThemePart{ + "env": {"#22535d"}, + "clo": {"#000", "#ff2500", "#ff2500", "#fff", "#fff"}, + "head": {"#a76c44"}, + "mouth": {"#191919", "#191919"}, + "eyes": {"#000", "none", "#000"}, + "top": {"none", "#00efff", "none"}, + }, + }, + + // Blond主题 + style.BlondStyle: { + ThemePart{ + "env": {"#2668DC"}, + "clo": {"#2385c6", "#b8d0e0", "#b8d0e0"}, + "head": {"#ad8a60"}, + "mouth": {"#000", "#4d4d4d"}, + "eyes": {"#7fb5a2", "#d1eddf", "#301e19"}, + "top": {"#fff510", "#fff510"}, + }, + ThemePart{ + "env": {"#643869"}, + "clo": {"#D67D1B", "#b8d0e0", "#b8d0e0"}, + "head": {"#CC985A", "none0000"}, + "mouth": {"#000", "#ececec"}, + "eyes": {"#1f2644", "#9b97ce", "#301e19"}, + "top": {"#00eaff", "none"}, + }, + ThemePart{ + "env": {"#F599FF"}, + "clo": {"#2823C6", "#b8d0e0", "#b8d0e0"}, + "head": {"#C7873A"}, + "mouth": {"#000", "#4d4d4d"}, + "eyes": {"#581b1b", "#FF8B8B", "#000"}, + "top": {"none", "#9c0092"}, + }, + }, + + // Ateam主题 + style.AteamStyle: { + ThemePart{ + "env": {"#d10084"}, + "clo": {"#efedee", "#00a1e0", "#00a1e0", "#efedee", "#ffce1c"}, + "head": {"#b35f49"}, + "mouth": {"#3a484a", "#000"}, + "eyes": {"#000"}, + "top": {"#000", "none", "#000", "none"}, + }, + ThemePart{ + "env": {"#E6C117"}, + "clo": {"#efedee", "#ec0033", "#ec0033", "#efedee", "#f2ff05"}, + "head": {"#ffc016"}, + "mouth": {"#4a3737", "#000"}, + "eyes": {"#000"}, + "top": {"#ffe900", "#ffe900", "none", "#ffe900"}, + }, + ThemePart{ + "env": {"#1d8c00"}, + "clo": {"#e000cb", "#fff", "#fff", "#e000cb", "#ffce1c"}, + "head": {"#b96438"}, + "mouth": {"#000", "#000"}, + "eyes": {"#000"}, + "top": {"#53ffff", "#53ffff", "none", "none"}, + }, + }, + + // Rasta主题 + style.RastaStyle: { + ThemePart{ + "env": {"#fc0065"}, + "clo": {"#708913", "#fdea14", "#708913", "#fdea14", "#708913"}, + "head": {"#DEA561"}, + "mouth": {"#444", "#000"}, + "eyes": {"#000"}, + "top": {"#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f"}, + }, + ThemePart{ + "env": {"#81f72e"}, + "clo": {"#ff0000", "#ffc107", "#ff0000", "#ffc107", "#ff0000"}, + "head": {"#ef9831"}, + "mouth": {"#6b0000", "#000"}, + "eyes": {"#000"}, + "top": {"#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "none", "none", "none", "none"}, + }, + ThemePart{ + "env": {"#00D872"}, + "clo": {"#590D00", "#FD1336", "#590D00", "#FD1336", "#590D00"}, + "head": {"#c36c00"}, + "mouth": {"#56442b", "#000"}, + "eyes": {"#000"}, + "top": {"#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "none", "none", "none", "none", "none", "none", "none", "none"}, + }, + }, + + //Meta主题 + style.MetaStyle: { + ThemePart{ + "env": {"#111"}, + "clo": {"#000", "#00FFFF"}, + "head": {"#755227"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "#008a", "aqua"}, + "top": {"#fff", "#fff", "#fff", "#fff", "#fff"}, + }, + ThemePart{ + "env": {"#00D0D4"}, + "clo": {"#000", "#fff"}, + "head": {"#755227"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "#1df7ffa3", "#fcff2c"}, + "top": {"#fff539", "none", "#fff539", "none", "#fff539"}, + }, + ThemePart{ + "env": {"#DC75FF"}, + "clo": {"#000", "#FFBDEC"}, + "head": {"#997549"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "black", "aqua"}, + "top": {"#00fffd", "none", "none", "none", "none"}, + }, + }, + // Square主题 + style.SquareStyle: { + ThemePart{ + "env": {"#111"}, + "clo": {"#000", "#00FFFF"}, + "head": {"#755227"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "#008a", "aqua"}, + "top": {"#fff", "#fff", "#fff", "#fff", "#fff"}, + }, + ThemePart{ + "env": {"#00D0D4"}, + "clo": {"#000", "#fff"}, + "head": {"#755227"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "#1df7ffa3", "#fcff2c"}, + "top": {"#fff539", "none", "#fff539", "none", "#fff539"}, + }, + ThemePart{ + "env": {"#DC75FF"}, + "clo": {"#000", "#FFBDEC"}, + "head": {"#997549"}, + "mouth": {"#fff", "#000"}, + "eyes": {"black", "black", "aqua"}, + "top": {"#00fffd", "none", "none", "none", "none"}, + }, + }, +} + +// initThemes 初始化主题数据 +func (m *Manager) initThemes() { + + for _, theme := range []style.StyleType{ + style.RoboStyle, + style.GirlStyle, + style.BlondeStyle, + style.GuyStyle, + style.CountryStyle, + style.GeeknotStyle, + style.AsianStyle, + style.PunkStyle, + style.AfrohairStyle, + style.NormieFemaleStyle, + style.OlderStyle, + style.FirehairStyle, + style.BlondStyle, + style.AteamStyle, + style.RastaStyle, + style.MetaStyle, + style.SquareStyle, + } { + if themeSet, exists := defaultThemeSet[theme]; exists { + m.AddTheme(themeSet) + } + } +} diff --git a/theme/theme.go b/theme/theme.go new file mode 100644 index 0000000..aeda25d --- /dev/null +++ b/theme/theme.go @@ -0,0 +1,72 @@ +package theme + +import ( + "github.com/landaiqing/go-pixelnebula/errors" +) + +// ColorScheme 表示一个颜色方案,包含多个颜色 +type ColorScheme []string + +// ThemePart 表示主题的一个部分,包含多个组件的颜色方案 +type ThemePart map[string]ColorScheme + +// Theme 表示一个完整的主题,包含多个部分 +type Theme []ThemePart + +// Manager 主题管理器,负责管理所有主题 +type Manager struct { + themes []Theme +} + +// NewThemeManager 创建一个新的主题管理器 +func NewThemeManager() *Manager { + m := &Manager{} + m.initThemes() + return m +} + +// GetTheme 获取指定索引的主题 +func (m *Manager) GetTheme(themeIndex, partIndex int) (ThemePart, error) { + if themeIndex < 0 || themeIndex >= len(m.themes) { + return nil, errors.ErrInvalidTheme + } + + theme := m.themes[themeIndex] + if partIndex < 0 || partIndex >= len(theme) { + return nil, errors.ErrInvalidPart + } + + return theme[partIndex], nil +} + +// StyleCount 返回主题数量 +func (m *Manager) StyleCount() int { + return len(m.themes) +} + +// ThemeCount 返回指定主题的部分数量 +func (m *Manager) ThemeCount(themeIndex int) int { + if themeIndex < 0 || themeIndex >= len(m.themes) { + return 0 + } + return len(m.themes[themeIndex]) +} + +// AddTheme 添加一个新主题 +func (m *Manager) AddTheme(theme Theme) int { + m.themes = append(m.themes, theme) + return len(m.themes) - 1 +} + +// CustomizeTheme 自定义主题 +func (m *Manager) CustomizeTheme(theme []Theme) { + m.themes = theme +} + +// GetThemeCountByStyle 获取指定风格索引下的主题数量 +func (m *Manager) GetThemeCountByStyle(styleIndex int) int { + if styleIndex < 0 || styleIndex >= len(m.themes) { + return 0 + } + return len(m.themes[styleIndex]) +}