7 Commits

Author SHA1 Message Date
fcc5e22e17 📄 Change the license 2025-07-18 19:17:36 +08:00
ec0ba0e9d0 🐛 Fixed errors caused by structure changes 2025-04-19 13:37:27 +08:00
c3220c73db Add batch build method 2025-04-19 13:30:14 +08:00
6e21263278 Performance optimization 2025-04-18 17:24:18 +08:00
5f8a1ae535 Add helper methods 2025-04-18 16:47:08 +08:00
db271eedf7 Add more themes and styles 2025-04-18 11:59:55 +08:00
e73adf408e 📝 Update README.md 2025-03-19 21:25:57 +08:00
27 changed files with 2026 additions and 358 deletions

83
LICENSE
View File

@@ -1,73 +1,18 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2025 landaiqing
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
"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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -18,10 +18,9 @@
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
<a href="https://goreportcard.com/"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
<a href="https://goreportcard.com/report/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square&logo=github" alt="License"></a>
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-1.0.0-blue?style=flat-square&logo=github" alt="Version"></a>
<a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&logo=github" alt="PRs Welcome"></a>
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
</div>
<p align="center" style="color: #ffffff;background: rgba(255, 255, 255, 0.1);padding: 15px; border-radius: 10px; ">

View File

@@ -18,10 +18,9 @@
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
<a href="https://goreportcard.com/"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
<a href="https://goreportcard.com/report/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square&logo=github" alt="License"></a>
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-1.0.0-blue?style=flat-square&logo=github" alt="Version"></a>
<a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&logo=github" alt="PRs Welcome"></a>
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
</div>
<p align="center" style="color: #ffffff;background: rgba(255, 255, 255, 0.1);padding: 15px; border-radius: 10px; ">
@@ -616,7 +615,7 @@ go test -bench=. -benchmem
## 📄 许可证
该项目采用 MIT 许可证 - 详情请查看 [LICENSE](LICENSE) 文件。
该项目采用 MIT 许可证 - 详情请查看 [LICENSE](LICENSE) 文件。
<hr/>

View File

@@ -2,6 +2,23 @@ package animation
import (
"strings"
"sync"
)
var (
// 字符串构建器对象池
animationBuilderPool = sync.Pool{
New: func() interface{} {
return new(strings.Builder)
},
}
// 动画映射对象池
animationMapPool = sync.Pool{
New: func() interface{} {
return make(map[string]string)
},
}
)
// AnimationType 表示动画类型
@@ -26,6 +43,8 @@ type Animation interface {
GenerateSVG() string
// GetTargetID 获取目标元素ID
GetTargetID() string
// GetType 获取动画类型
GetType() AnimationType
}
// BaseAnimation 基础动画结构,包含所有动画共有的属性
@@ -43,20 +62,20 @@ func (a *BaseAnimation) GetTargetID() string {
return a.TargetID
}
// GetType 获取动画类型
func (a *BaseAnimation) GetType() AnimationType {
return a.Type
}
// 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),
animations: make([]Animation, 0, 10), // 预分配容量
}
}
@@ -65,25 +84,63 @@ func (m *Manager) AddAnimation(animation Animation) {
m.animations = append(m.animations, animation)
}
// GetAnimations 获取所有动画
func (m *Manager) GetAnimations() []Animation {
return m.animations
}
// GetAnimationsByType 获取指定类型的动画
func (m *Manager) GetAnimationsByType(animType AnimationType) []Animation {
// 预分配足够容量
result := make([]Animation, 0, len(m.animations)/2)
for _, anim := range m.animations {
if anim.GetType() == animType {
result = append(result, anim)
}
}
return result
}
// GenerateSVGAnimations 生成SVG动画代码
func (m *Manager) GenerateSVGAnimations() string {
if len(m.animations) == 0 {
return ""
}
var sb strings.Builder
// 从对象池获取构建器
sb := animationBuilderPool.Get().(*strings.Builder)
sb.Reset()
sb.Grow(1024) // 预分配足够的容量
defer animationBuilderPool.Put(sb)
// 添加SVG命名空间声明
sb.WriteString("<defs>\n")
// 用于存储需要放在defs中的定义
var defsContent strings.Builder
// 用于存储需要直接添加到SVG中的动画元素
var animationsContent strings.Builder
// 用于存储旋转动画的映射键为目标元素ID
rotateAnimations := make(map[string]string)
// 获取定义内容构建器
defsContent := animationBuilderPool.Get().(*strings.Builder)
defsContent.Reset()
defsContent.Grow(512)
defer animationBuilderPool.Put(defsContent)
// 处理所有动画
// 获取动画内容构建器
animationsContent := animationBuilderPool.Get().(*strings.Builder)
animationsContent.Reset()
animationsContent.Grow(512)
defer animationBuilderPool.Put(animationsContent)
// 获取旋转动画映射
rotateAnimations := animationMapPool.Get().(map[string]string)
defer func() {
// 清空映射并归还
for k := range rotateAnimations {
delete(rotateAnimations, k)
}
animationMapPool.Put(rotateAnimations)
}()
// 一次性处理所有动画以减少循环开销
for _, anim := range m.animations {
svgCode := anim.GenerateSVG()
if svgCode == "" {
@@ -91,16 +148,15 @@ func (m *Manager) GenerateSVGAnimations() string {
}
// 根据动画类型决定放置位置
switch a := anim.(type) {
case *GradientAnimation:
switch anim.GetType() {
case Gradient:
// 渐变定义需要放在defs中
defsContent.WriteString(svgCode)
case *RotateAnimation:
case Rotate:
// 旋转动画需要包裹目标元素,先存储起来
// 提取animateTransform标签
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
rotateAnimations[a.GetTargetID()] = svgCode[start : start+end+2]
rotateAnimations[anim.GetTargetID()] = svgCode[start : start+end+2]
}
}
default:

View File

@@ -0,0 +1,298 @@
package benchmark
import (
"fmt"
"testing"
"github.com/landaiqing/go-pixelnebula"
"github.com/landaiqing/go-pixelnebula/style"
)
// BenchmarkBatchGeneration 测试不同批量大小的SVG生成性能
func BenchmarkBatchGeneration(b *testing.B) {
batchSizes := []int{10, 50, 100, 500}
for _, size := range batchSizes {
// 顺序生成的测试
b.Run(fmt.Sprintf("Sequential_Size_%d", size), 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.WithParallelRender(false) // 禁用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
// 并行生成的测试
b.Run(fmt.Sprintf("Parallel_Size_%d", size), 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.WithParallelRender(true) // 启用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkBatchWithDifferentConcurrencyLevels 测试不同并发级别的批量生成性能
func BenchmarkBatchWithDifferentConcurrencyLevels(b *testing.B) {
batchSize := 100
concurrencyLevels := []int{2, 4, 8, 16, 32}
for _, level := range concurrencyLevels {
b.Run(fmt.Sprintf("ConcurrencyLevel_%d", level), 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.WithParallelRender(true) // 启用并行渲染
pn.WithConcurrencyPool(level) // 设置并发级别
// 准备ID列表
ids := generateIDs(batchSize)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkBatchToBase64 测试批量转换为Base64的性能
func BenchmarkBatchToBase64(b *testing.B) {
batchSizes := []int{10, 50, 100}
for _, size := range batchSizes {
// 顺序转换
b.Run(fmt.Sprintf("Sequential_Size_%d", size), 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.WithParallelRender(false) // 禁用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量生成和转换
_, err := pn.GenerateBatchBase64(ids, false, nil, 231, 231)
if err != nil {
b.Fatal(err)
}
}
})
// 并行转换
b.Run(fmt.Sprintf("Parallel_Size_%d", size), 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.WithParallelRender(true) // 启用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量生成和转换
_, err := pn.GenerateBatchBase64(ids, false, nil, 231, 231)
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkBatchWithCache 测试使用缓存的批量生成性能
func BenchmarkBatchWithCache(b *testing.B) {
batchSize := 100
b.Run("WithoutCache", 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.WithParallelRender(true) // 启用并行渲染
// 每次使用固定的ID列表
ids := generateFixedIDs(batchSize)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
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.WithParallelRender(true) // 启用并行渲染
pn.WithDefaultCache() // 启用缓存
// 使用固定ID列表
ids := generateFixedIDs(batchSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 执行批量生成由于固定ID将使用缓存
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkSaveToFiles 测试批量保存文件的性能
func BenchmarkSaveToFiles(b *testing.B) {
// 跳过实际文件写入,仅测试内存操作部分
b.Skip("Skipping file I/O benchmark to avoid disk writes")
batchSizes := []int{10, 50, 100}
for _, size := range batchSizes {
// 顺序保存
b.Run(fmt.Sprintf("Sequential_Size_%d", size), 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.WithParallelRender(false) // 禁用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量保存(使用临时目录)
_, err := pn.SaveBatchToFiles(ids, false, nil, "/tmp/benchmark/avatar_%s.svg")
if err != nil {
b.Fatal(err)
}
}
})
// 并行保存
b.Run(fmt.Sprintf("Parallel_Size_%d", size), 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.WithParallelRender(true) // 启用并行渲染
// 准备ID列表
ids := generateIDs(size)
// 执行批量保存(使用临时目录)
_, err := pn.SaveBatchToFiles(ids, false, nil, "/tmp/benchmark/avatar_%s.svg")
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkMemoryUsageBatch 测试批量生成的内存使用情况
func BenchmarkMemoryUsageBatch(b *testing.B) {
batchSize := 100
// 测试顺序生成的内存使用
b.Run("SequentialBatch", 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.WithParallelRender(false) // 禁用并行渲染
// 准备ID列表
ids := generateIDs(batchSize)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
// 测试并行生成的内存使用
b.Run("ParallelBatch", 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.WithParallelRender(true) // 启用并行渲染
// 准备ID列表
ids := generateIDs(batchSize)
// 执行批量生成
_, err := pn.GenerateBatch(ids, false, nil)
if err != nil {
b.Fatal(err)
}
}
})
}
// 生成指定数量的随机ID
func generateIDs(count int) []string {
ids := make([]string, count)
for i := 0; i < count; i++ {
ids[i] = fmt.Sprintf("batch-id-%d", i)
}
return ids
}
// 生成固定的ID集合用于缓存测试
func generateFixedIDs(count int) []string {
ids := make([]string, count)
for i := 0; i < count; i++ {
ids[i] = fmt.Sprintf("fixed-id-%d", i)
}
return ids
}

View File

@@ -83,13 +83,33 @@ func BenchmarkCacheSizes(b *testing.B) {
// BenchmarkCacheCompression 测试缓存压缩对性能的影响
func BenchmarkCacheCompression(b *testing.B) {
compressionLevels := []struct {
name string
level int
name string
options cache.CompressOptions
}{
{"NoCompression", 0},
{"LowCompression", 3},
{"MediumCompression", 6},
{"HighCompression", 9},
{
name: "NoCompression",
options: cache.CompressOptions{
Enabled: false,
},
},
{
name: "DefaultCompression",
options: cache.CompressOptions{
Enabled: true,
Level: 5, // 默认压缩级别
MinSize: 1024, // 最小压缩大小
CompressionRatio: 0.8, // 最小压缩比率
},
},
{
name: "BestCompression",
options: cache.CompressOptions{
Enabled: true,
Level: 9, // 最高压缩级别
MinSize: 1024, // 最小压缩大小
CompressionRatio: 0.8, // 最小压缩比率
},
},
}
for _, cl := range compressionLevels {
@@ -100,12 +120,8 @@ func BenchmarkCacheCompression(b *testing.B) {
pn.WithSize(231, 231)
pn.WithDefaultCache()
if cl.level > 0 {
pn.WithCompression(cache.CompressOptions{
Enabled: true,
Level: cl.level,
MinSizeBytes: 100,
})
if cl.options.Enabled {
pn.WithCompression(cl.options)
}
b.ResetTimer()

164
cache/cache.go vendored
View File

@@ -2,10 +2,28 @@ package cache
import (
"container/list"
"strconv"
"strings"
"sync"
"time"
)
var (
// 字符串构建器对象池
stringBuilderPool = sync.Pool{
New: func() interface{} {
return new(strings.Builder)
},
}
// 缓存项对象池
cacheItemPool = sync.Pool{
New: func() interface{} {
return new(CacheItem)
},
}
)
// CacheOptions 缓存配置选项
type CacheOptions struct {
Enabled bool // 是否启用缓存
@@ -14,6 +32,8 @@ type CacheOptions struct {
EvictionType string // 缓存淘汰策略,支持"lru"(最近最少使用)和"fifo"(先进先出)
Compression CompressOptions // 压缩选项
Monitoring MonitorOptions // 监控选项
// 新增选项:是否启用预热
Preheating bool // 是否启用缓存预热
}
// DefaultCacheOptions 默认缓存配置
@@ -24,6 +44,7 @@ var DefaultCacheOptions = CacheOptions{
EvictionType: "lru", // 默认使用LRU淘汰策略
Compression: DefaultCompressOptions, // 默认压缩选项
Monitoring: DefaultMonitorOptions, // 默认监控选项
Preheating: false, // 默认不启用预热
}
// CacheKey 缓存键结构
@@ -34,6 +55,37 @@ type CacheKey struct {
Part int
}
// String 返回缓存键的字符串表示
func (k CacheKey) String() string {
// 从对象池获取构建器
sb := stringBuilderPool.Get().(*strings.Builder)
sb.Reset()
// 预分配足够容量
sb.Grow(64)
// 构建键字符串
sb.WriteString(k.Id)
sb.WriteByte('_')
if k.SansEnv {
sb.WriteString("true")
} else {
sb.WriteString("false")
}
sb.WriteByte('_')
sb.WriteString(strconv.Itoa(k.Theme))
sb.WriteByte('_')
sb.WriteString(strconv.Itoa(k.Part))
// 获取结果
result := sb.String()
// 归还构建器
stringBuilderPool.Put(sb)
return result
}
// CacheItem 缓存项结构
type CacheItem struct {
SVG string // SVG内容
@@ -43,6 +95,15 @@ type CacheItem struct {
LastUsed time.Time // 最后使用时间
}
// Reset 重置缓存项以便重用
func (item *CacheItem) Reset() {
item.SVG = ""
item.Compressed = nil
item.IsCompressed = false
item.CreatedAt = time.Time{}
item.LastUsed = time.Time{}
}
// PNCache SVG缓存结构
type PNCache struct {
Options CacheOptions
@@ -58,7 +119,7 @@ type PNCache struct {
func NewCache(options CacheOptions) *PNCache {
cache := &PNCache{
Options: options,
Items: make(map[CacheKey]*list.Element),
Items: make(map[CacheKey]*list.Element, options.Size),
EvictionList: list.New(),
Hits: 0,
Misses: 0,
@@ -103,6 +164,11 @@ func (c *PNCache) Get(key CacheKey) (string, bool) {
// 删除过期项
c.EvictionList.Remove(element)
delete(c.Items, key)
// 将缓存项归还到对象池
cacheItem.Reset()
cacheItemPool.Put(cacheItem)
c.Misses++
return "", false
}
@@ -178,15 +244,15 @@ func (c *PNCache) Set(key CacheKey, svg string) {
c.evictItem()
}
// 创建新的缓存项
// 从对象池获取新的缓存项
now := time.Now()
cacheItem := &CacheItem{
SVG: svg,
Compressed: compressed,
IsCompressed: isCompressed,
CreatedAt: now,
LastUsed: now,
}
cacheItem := cacheItemPool.Get().(*CacheItem)
cacheItem.Reset()
cacheItem.SVG = svg
cacheItem.Compressed = compressed
cacheItem.IsCompressed = isCompressed
cacheItem.CreatedAt = now
cacheItem.LastUsed = now
// 添加到链表和映射
element := c.EvictionList.PushFront(cacheItem)
@@ -214,6 +280,9 @@ func (c *PNCache) evictItem() {
// 从链表中移除
c.EvictionList.Remove(element)
// 获取缓存项并归还到对象池
cacheItem := element.Value.(*CacheItem)
// 从映射中找到并删除对应的键
for k, v := range c.Items {
if v == element {
@@ -221,6 +290,10 @@ func (c *PNCache) evictItem() {
break
}
}
// 重置并归还缓存项
cacheItem.Reset()
cacheItemPool.Put(cacheItem)
}
}
@@ -229,7 +302,14 @@ func (c *PNCache) Clear() {
c.Mutex.Lock()
defer c.Mutex.Unlock()
c.Items = make(map[CacheKey]*list.Element)
// 归还所有缓存项到对象池
for e := c.EvictionList.Front(); e != nil; e = e.Next() {
item := e.Value.(*CacheItem)
item.Reset()
cacheItemPool.Put(item)
}
c.Items = make(map[CacheKey]*list.Element, c.Options.Size)
c.EvictionList = list.New()
c.Hits = 0
c.Misses = 0
@@ -277,6 +357,9 @@ func (c *PNCache) RemoveExpired() int {
c.EvictionList.Remove(element)
// 从映射中删除
delete(c.Items, key)
// 归还缓存项到对象池
cacheItem.Reset()
cacheItemPool.Put(cacheItem)
count++
}
}
@@ -311,3 +394,64 @@ func (c *PNCache) UpdateOptions(options CacheOptions) {
}
}
}
// GetAllItems 获取所有缓存项
func (c *PNCache) GetAllItems() map[CacheKey]*CacheItem {
c.Mutex.RLock()
defer c.Mutex.RUnlock()
// 创建一个副本避免直接暴露内部map
items := make(map[CacheKey]*CacheItem, len(c.Items))
// 从EvictionList获取所有缓存项
for e := c.EvictionList.Front(); e != nil; e = e.Next() {
item := e.Value.(*CacheItem)
// 查找对应的key
for k, v := range c.Items {
if v == e {
// 创建项的副本
itemCopy := *item
items[k] = &itemCopy
break
}
}
}
return items
}
// GetMonitor 获取缓存监控器
func (c *PNCache) GetMonitor() *Monitor {
c.Mutex.RLock()
defer c.Mutex.RUnlock()
return c.Monitor
}
// DeleteItem 删除指定的缓存项
func (c *PNCache) DeleteItem(key CacheKey) bool {
c.Mutex.Lock()
defer c.Mutex.Unlock()
// 查找对应的缓存项
element, found := c.Items[key]
if !found {
return false
}
// 获取缓存项
cacheItem := element.Value.(*CacheItem)
// 从链表中移除
c.EvictionList.Remove(element)
// 从映射中删除
delete(c.Items, key)
// 重置并归还缓存项
cacheItem.Reset()
cacheItemPool.Put(cacheItem)
return true
}

219
cache/compress.go vendored
View File

@@ -5,60 +5,95 @@ import (
"compress/gzip"
"io"
"strings"
"sync"
)
// CompressOptions 压缩选项
var (
// 字符串构建器对象池
optimizeBuilderPool = sync.Pool{
New: func() interface{} {
return new(strings.Builder)
},
}
// Gzip Writer对象池
gzipWriterPool = sync.Pool{
New: func() interface{} {
writer, _ := gzip.NewWriterLevel(nil, gzip.BestCompression)
return writer
},
}
// Gzip Reader对象池
gzipReaderPool = sync.Pool{
New: func() interface{} {
return new(gzip.Reader)
},
}
// 字节缓冲区对象池
bytesBufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
)
// CompressOptions 压缩配置选项
type CompressOptions struct {
Enabled bool // 是否启用压缩
Level int // 压缩级别 (1-9)1为最快压缩9为最佳压缩
MinSizeBytes int // 最小压缩大小,小于此大小的数据不进行压缩
Ratio float64 // 压缩比阈值,压缩后大小/原始大小,小于此值才保存压缩结果
Enabled bool // 是否启用压缩
Level int // 压缩级别,范围从-2(不压缩)到9(最高压缩)
MinSize int // 最小压缩大小,小于此大小的SVG不压缩
CompressionRatio float64 // 最小压缩比,压缩后大小/原始大小,小于此比率才使用压缩结果
}
// DefaultCompressOptions 默认压缩选项
// DefaultCompressOptions 默认压缩配置
var DefaultCompressOptions = CompressOptions{
Enabled: true,
Level: 6, // 默认压缩级别为6平衡压缩率和性能
MinSizeBytes: 100, // 默认最小压缩大小为100字节
Ratio: 0.9, // 默认压缩比阈值为0.9即至少要压缩到原始大小的90%以下才保存压缩结果
Enabled: true,
Level: 9, // 默认使用最高压缩级别
MinSize: 1024, // 默认最小压缩大小为1KB
CompressionRatio: 0.8, // 默认最小压缩比为0.8
}
// CompressSVG 压缩SVG数据
// 返回压缩后的数据和是否进行了压缩
// CompressSVG 压缩SVG字符串
func CompressSVG(svg string, options CompressOptions) ([]byte, bool) {
if !options.Enabled || len(svg) < options.MinSizeBytes {
if !options.Enabled {
return []byte(svg), false
}
// 创建一个bytes.Buffer来存储压缩数据
var buf bytes.Buffer
// 创建一个gzip.Writer设置压缩级别
writer, err := gzip.NewWriterLevel(&buf, options.Level)
if err != nil {
// 如果SVG太小不进行压缩
if len(svg) < options.MinSize {
return []byte(svg), false
}
// 写入SVG数据
_, err = writer.Write([]byte(svg))
if err != nil {
// 从对象池获取缓冲区
buf := bytesBufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bytesBufferPool.Put(buf)
// 从对象池获取gzip写入器
writer := gzipWriterPool.Get().(*gzip.Writer)
writer.Reset(buf)
defer gzipWriterPool.Put(writer)
// 写入数据
if _, err := writer.Write([]byte(svg)); err != nil {
return []byte(svg), false
}
// 关闭writer确保所有数据都被写入
err = writer.Close()
if err != nil {
// 关闭写入
if err := writer.Close(); err != nil {
return []byte(svg), false
}
// 获取压缩后的数据
compressed := buf.Bytes()
compressed := make([]byte, buf.Len())
copy(compressed, buf.Bytes())
// 计算压缩比
// 计算压缩比
ratio := float64(len(compressed)) / float64(len(svg))
// 如果压缩比不理想,返回原始数据
if ratio >= options.Ratio {
if ratio > options.CompressionRatio {
// 压缩效果不好,返回原始数据
return []byte(svg), false
}
@@ -77,7 +112,12 @@ func DecompressSVG(data []byte, isCompressed bool) (string, error) {
return string(data), nil
}
// 创建一个gzip.Reader
// 获取缓冲区
buf := bytesBufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bytesBufferPool.Put(buf)
// 创建Reader
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return string(data), err
@@ -85,12 +125,11 @@ func DecompressSVG(data []byte, isCompressed bool) (string, error) {
defer reader.Close()
// 读取解压缩后的数据
decompressed, err := io.ReadAll(reader)
if err != nil {
if _, err := io.Copy(buf, reader); err != nil {
return string(data), err
}
return string(decompressed), nil
return buf.String(), nil
}
// isGzipped 检查数据是否为gzip格式
@@ -101,6 +140,11 @@ func isGzipped(data []byte) bool {
// OptimizeSVG 优化SVG字符串移除不必要的空白和注释
func OptimizeSVG(svg string) string {
// 如果SVG太小不进行优化
if len(svg) < 100 {
return svg
}
// 移除XML注释
svg = removeXMLComments(svg)
@@ -110,33 +154,100 @@ func OptimizeSVG(svg string) string {
return svg
}
// removeXMLComments 移除XML注释
// removeXMLComments 移除XML注释 - 优化版本
func removeXMLComments(svg string) string {
for {
start := strings.Index(svg, "<!--")
if start == -1 {
break
}
end := strings.Index(svg[start:], "-->") + start
if end > start {
svg = svg[:start] + svg[end+3:]
// 获取构建器
sb := optimizeBuilderPool.Get().(*strings.Builder)
sb.Reset()
sb.Grow(len(svg))
defer optimizeBuilderPool.Put(sb)
for i := 0; i < len(svg); {
// 检查是否遇到注释开始
if i+3 < len(svg) && svg[i] == '<' && svg[i+1] == '!' && svg[i+2] == '-' && svg[i+3] == '-' {
// 查找注释结束
end := i + 4
for end+2 <= len(svg) {
if svg[end] == '-' && svg[end+1] == '-' && svg[end+2] == '>' {
end += 3
break
}
end++
}
// 跳过注释
i = end
} else {
break
// 添加非注释字符
sb.WriteByte(svg[i])
i++
}
}
return svg
return sb.String()
}
// removeExtraWhitespace 移除多余的空白
// removeExtraWhitespace 移除多余的空白 - 优化版本
func removeExtraWhitespace(svg string) string {
// 替换多个空白字符为单个空格
svg = strings.Join(strings.Fields(svg), " ")
// 获取构建器
sb := optimizeBuilderPool.Get().(*strings.Builder)
sb.Reset()
sb.Grow(len(svg))
defer optimizeBuilderPool.Put(sb)
// 优化常见的SVG标签周围的空白
svg = strings.ReplaceAll(svg, "> <", "><")
svg = strings.ReplaceAll(svg, " />", "/>")
svg = strings.ReplaceAll(svg, " =", "=")
svg = strings.ReplaceAll(svg, "= ", "=")
inTag := false
inQuote := false
quoteChar := byte(0)
lastWasSpace := false
return svg
for i := 0; i < len(svg); i++ {
c := svg[i]
// 处理引号内的内容
if inQuote {
sb.WriteByte(c)
if c == quoteChar {
inQuote = false
}
continue
}
// 处理标签
if c == '<' {
inTag = true
lastWasSpace = false
sb.WriteByte(c)
continue
}
if c == '>' {
inTag = false
lastWasSpace = false
sb.WriteByte(c)
continue
}
// 处理引号开始
if (c == '"' || c == '\'') && inTag {
inQuote = true
quoteChar = c
sb.WriteByte(c)
continue
}
// 处理空白字符
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
// 压缩连续空白
if !lastWasSpace && (inTag || i > 0 && svg[i-1] != '>') {
sb.WriteByte(' ')
lastWasSpace = true
}
continue
}
// 处理普通字符
lastWasSpace = false
sb.WriteByte(c)
}
return sb.String()
}

45
cache/monitor.go vendored
View File

@@ -99,6 +99,18 @@ func (m *Monitor) GetStats() CacheStats {
return m.stats
}
// GetSampleHistory 获取采样历史
func (m *Monitor) GetSampleHistory() []CacheStats {
m.mutex.RLock()
defer m.mutex.RUnlock()
// 创建一个副本
result := make([]CacheStats, len(m.sampleHistory))
copy(result, m.sampleHistory)
return result
}
// monitorRoutine 监控例程
func (m *Monitor) monitorRoutine() {
sampleTicker := time.NewTicker(m.options.SampleInterval)
@@ -128,8 +140,37 @@ func (m *Monitor) collectSample() {
hits, misses, hitRate := m.cache.Stats()
size := m.cache.Size()
// 估算内存使用量(简化计算,实际应用中可能需要更精确的方法)
memoryUsage := int64(size * 1024) // 假设每个缓存项平均占用1KB
// 估算内存使用量
var memoryUsage int64
// 获取所有缓存项
items := m.cache.GetAllItems()
for _, item := range items {
itemSize := int64(0)
// 计算SVG字符串占用的内存
if !item.IsCompressed {
itemSize += int64(len(item.SVG) * 2) // Go中字符串以UTF-16编码存储每个字符占2字节
}
// 计算压缩数据占用的内存
if item.IsCompressed {
itemSize += int64(len(item.Compressed))
}
// 添加对象结构体本身的大小估计
// CacheItem结构体的基本大小约为40字节2个时间戳、1个布尔值、2个引用
itemSize += 40
memoryUsage += itemSize
}
// 添加缓存管理结构的开销估计 (映射表、双向链表等)
// 每个map项约24字节开销每个链表节点约16字节
mapOverhead := int64(size * 24)
listOverhead := int64(size * 16)
memoryUsage += mapOverhead + listOverhead
// 创建新的统计样本
newStat := CacheStats{

View File

@@ -172,9 +172,9 @@ func monitoredCacheExample() {
func compressedCacheExample() {
// 创建压缩选项
compressOptions := cache.CompressOptions{
Enabled: true,
Level: 6,
MinSizeBytes: 100, // 最小压缩大小 (字节)
Enabled: true,
Level: 6,
MinSize: 100, // 最小压缩大小 (字节)
}
// 创建一个带默认缓存和压缩的PixelNebula实例

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,9 @@ func TestPixelNebula(t *testing.T) {
}
// 打印可用的风格和主题数量
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))
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 {
@@ -46,9 +46,9 @@ func TestPixelNebula(t *testing.T) {
}
// 打印调试信息
pn.hasher.Reset()
pn.hasher.Write([]byte(id))
sum := pn.hasher.Sum(nil)
pn.Hasher.Reset()
pn.Hasher.Write([]byte(id))
sum := pn.Hasher.Sum(nil)
hashStr := hex.EncodeToString(sum)
// 提取数字
@@ -144,7 +144,8 @@ func TestDemo(t *testing.T) {
pn := NewPixelNebula()
// 设置风格和尺寸
pn.WithStyle(style.GirlStyle)
pn.WithStyle(style.GuyStyle)
pn.WithTheme(1)
pn.WithSize(231, 231)
// 生成 SVG 并保存到文件

14
style/cosmic_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// CosmicStyle 宇宙风格类型
const CosmicStyle StyleType = "cosmic"
// CosmicStyleShapes 宇宙风格形状集合
var CosmicStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m141.75 195a114.79 114.79 0 0 1 38 16.5 115.53 115.53 0 0 1-128.46 0 114.79 114.79 0 0 1 38-16.5l15.71 15.75h21z\" style=\"fill:#000;\"/><path d=\"m89.29 201a10 10 0 0 1 10-10 10 10 0 0 1 10 10 10 10 0 0 1-10 10 10 10 0 0 1-10-10zm45 7a5 5 0 0 1 5-5 5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5zm-25 10a3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3 3 3 0 0 1-3-3zm-30-5a2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2zm70-10a4 4 0 0 1 4-4 4 4 0 0 1 4 4 4 4 0 0 1-4 4 4 4 0 0 1-4-4z\" style=\"fill:#8426c7;\"/>",
TypeMouth: "<path id='mouth' d=\"m104 147a12 12 0 0 0 24 0h-24z\" style=\"fill:#ae00ff;\"/>",
TypeEyes: "<path id='eyes' d=\"m75 95a10 10 0 0 1 10 10 10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10 10zm80 0a10 10 0 0 1 10 10 10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10 10z\" style=\"fill:#5500ff;\"/><path d=\"m75 95a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5 5zm80 0a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5 5z\" style=\"fill:#00ffff;\"/>",
TypeTop: "<path id='top' d=\"m115.5 30c-27.5 0-50 22.5-50 50h100c0-27.5-22.5-50-50-50z\" style=\"fill:#000;\"/><path d=\"m83 40a3 3 0 0 1 3 3 3 3 0 0 1-3 3 3 3 0 0 1-3-3 3 3 0 0 1 3-3zm-10 15a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2zm65 0a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2zm-20-10a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5zm-70 25a4 4 0 0 1 4 4 4 4 0 0 1-4 4 4 4 0 0 1-4-4 4 4 0 0 1 4-4zm100 5a3 3 0 0 1 3 3 3 3 0 0 1-3 3 3 3 0 0 1-3-3 3 3 0 0 1 3-3z\" style=\"fill:#8426c7;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

14
style/ghost_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// GhostStyle 幽灵风格类型
const GhostStyle StyleType = "ghost"
// GhostStyleShapes 幽灵风格形状集合
var GhostStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m141.75 195a114.79 114.79 0 0 1 38 16.5 115.53 115.53 0 0 1-128.46 0 114.79 114.79 0 0 1 38-16.5c0 0 12.21 15.75 26.25 15.75s26.25-15.75 26.25-15.75z\" style=\"fill:#fff;fill-opacity:0.3;\"/><path d=\"m84.29 205c5-10 9-15 15-20 5 5 9 10 13 20-3 5-8 8-13 8s-11-3-15-8z\" style=\"fill:#fff;fill-opacity:0.2;\"/>",
TypeMouth: "<path id='mouth' d=\"m115.5 145c-5 0-10 5-10 10 0-5 5-5 10-5s10 0 10 5c0-5-5-10-10-10z\" style=\"fill:#000;fill-opacity:0.3;\"/>",
TypeEyes: "<path id='eyes' d=\"m85 95c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm60 0c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10z\" style=\"fill:#000;fill-opacity:0.3;\"/>",
TypeTop: "<path id='top' d=\"m65.5 70c0-20 20-40 50-40s50 20 50 40c0 0-10-10-50-10s-50 10-50 10z\" style=\"fill:#fff;fill-opacity:0.3;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#fff;fill-opacity:0.5;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

View File

@@ -166,6 +166,24 @@ var defaultStyleSet = map[StyleType]StyleSet{
TypeHead: "<path id='head' d=\"m50 50h130v130h-130z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"m30 30h170v170h-170z\" style=\"fill:#01;\"/>",
},
// NeonStyle 风格
NeonStyle: NeonStyleShapes,
// PixelStyle 风格
PixelStyle: PixelStyleShapes,
// WatercolorStyle 风格
WatercolorStyle: WatercolorStyleShapes,
// MechStyle 风格
MechStyle: MechStyleShapes,
// CosmicStyle 风格
CosmicStyle: CosmicStyleShapes,
// GhostStyle 风格
GhostStyle: GhostStyleShapes,
}
// initShapes 初始化形状数据
@@ -188,6 +206,12 @@ func (m *Manager) initShapes() {
RastaStyle,
MetaStyle,
SquareStyle,
NeonStyle, // 霓虹风格
PixelStyle, // 像素风格
WatercolorStyle, // 水彩风格
MechStyle, // 机械风格
CosmicStyle, // 宇宙风格
GhostStyle, // 幽灵风格
} {
if styleSet, exists := defaultStyleSet[style]; exists {
m.AddStyleSet(styleSet)

14
style/mech_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// MechStyle 机械风格类型
const MechStyle StyleType = "mech"
// MechStyleShapes 机械风格形状集合
var MechStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m141.75 195a114.79 114.79 0 0 1 38 16.5 115.53 115.53 0 0 1-128.46 0 114.79 114.79 0 0 1 38-16.5l10 10h32.46l10-10z\" style=\"fill:#555;\"/><path d=\"m80 205h70v10h-70zm-10 10h90v5h-90zm5-15h10v5h-10zm60 0h10v5h-10zm-45 0h10v5h-10zm20 0h10v5h-10z\" style=\"fill:#777;\"/><path d=\"m115.5 205a5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5 5 5 0 0 1 5 5zm20 0a5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5 5 5 0 0 1 5 5zm-40 0a5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5 5 5 0 0 1 5 5z\" style=\"fill:#333;\"/>",
TypeMouth: "<path id='mouth' d=\"m95 140h40a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-40a5 5 0 0 1-5-5 5 5 0 0 1 5-5z\" style=\"fill:#222;\"/><path d=\"m100 140v10m30-10v10m-20-10v10m10-10v10\" style=\"fill:none;stroke:#555;stroke-width:1;\"/>",
TypeEyes: "<path id='eyes' d=\"m85 95a10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10-10 10 10 0 0 1 10 10zm80 0a10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10-10 10 10 0 0 1 10 10z\" style=\"fill:#333;\"/><path d=\"m77 95a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm8 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm72 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm8 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z\" style=\"fill:#73e8ff;\"/>",
TypeTop: "<path id='top' d=\"m70 65c5-5 85-5 90 0 5 5 10 15 10 20h-110c0-5 5-15 10-20z\" style=\"fill:#444;\"/><path d=\"m75 65h10a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-10a5 5 0 0 1-5-5 5 5 0 0 1 5-5zm60 0h10a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-10a5 5 0 0 1-5-5 5 5 0 0 1 5-5z\" style=\"fill:#777;\"/><path d=\"m95 70h50a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-50a5 5 0 0 1-5-5 5 5 0 0 1 5-5z\" style=\"fill:#222;\"/><path d=\"m100 70v10m40-10v10m-30-10v10m20-10v10m-10-10v10\" style=\"fill:none;stroke:#555;stroke-width:1;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

14
style/neon_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// NeonStyle 霓虹风格类型
const NeonStyle StyleType = "neon"
// NeonStyleShapes 霓虹风格形状集合
var NeonStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m141.75 195a114.79 114.79 0 0 1 38 16.5 115.53 115.53 0 0 1-128.46 0 114.79 114.79 0 0 1 38-16.5l20.71 20.75h11z\" style=\"fill:#1e1e1e;\"/><path d=\"m118.35 208.83-11.66 11.66 16.9 6.64 16.9-6.64-11.66-11.66z\" style=\"fill:#5c5c5c;\"/><path d=\"m119.94 227a3.23 3.23 0 0 1-1.73-0.5l-16.9-6.64a3.25 3.25 0 0 1-1.35-4.4 3.25 3.25 0 0 1 0.44-0.65l9.99-10a3.24 3.24 0 0 1 4.59-0.08 3.24 3.24 0 0 1 0.08 4.59l-7.17 7.17 11.05 4.35 11.05-4.35-7.17-7.17a3.24 3.24 0 0 1 0.08-4.59 3.24 3.24 0 0 1 4.59 0.08l9.99 10a3.25 3.25 0 0 1-0.91 5.05l-16.9 6.64a3.23 3.23 0 0 1-1.73 0.5z\" style=\"fill:#00f2ff;\"/>",
TypeMouth: "<path id='mouth' d=\"m105.5 147.75a10 10 0 0 0 20 0z\" style=\"fill:#ff00d4;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px;stroke:#00f2ff;\"/>",
TypeEyes: "<path id='eyes' d=\"m94.5 104.75a10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10-10 10 10 0 0 1 10 10zm52 0a10 10 0 0 1-10 10 10 10 0 0 1-10-10 10 10 0 0 1 10-10 10 10 0 0 1 10 10z\" style=\"fill:#ff00d4;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;stroke:#00f2ff;\"/><path d=\"m83.5 104.75a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm10 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm42 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm10 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z\" style=\"fill:#ffffff;\"/>",
TypeTop: "<path id='top' d=\"m115.5 47.75c-15.4 0-23.45 7.03-27.55 12.6-6.55 8.91-8.45 19.4-8.45 19.4h72c0 0-1.9-10.49-8.45-19.4-4.1-5.57-12.15-12.6-27.55-12.6z\" style=\"fill:#1e1e1e;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;stroke:#00f2ff;\"/><path d=\"m62.5 72.75 5 15m91-15-5 15\" style=\"fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px;stroke:#00f2ff;\"/><path d=\"m115.5 15.75v15m-20 5 5-15m35 15-5-15\" style=\"fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px;stroke:#ff00d4;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

14
style/pixel_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// PixelStyle 像素风格类型
const PixelStyle StyleType = "pixel"
// PixelStyleShapes 像素风格形状集合
var PixelStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m60 180h120v20h-120zm0 20h20v10h-20zm100 0h20v10h-20zm-80 0h20v10h-20zm20 0h20v10h-20zm20 0h20v10h-20z\" style=\"fill:#333;\"/>",
TypeMouth: "<path id='mouth' d=\"m100 150h30v5h-30zm5-5h20v5h-20z\" style=\"fill:#1a1a1a;\"/>",
TypeEyes: "<path id='eyes' d=\"m80 100h10v10h-10zm60 0h10v10h-10z\" style=\"fill:#1a1a1a;\"/>",
TypeTop: "<path id='top' d=\"m90 40h50v10h-50zm-10 10h70v10h-70zm-10 10h90v10h-90zm0 10h90v10h-90z\" style=\"fill:#333;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

View File

@@ -106,6 +106,12 @@ func (m *Manager) GetStyleIndex(style StyleType) (int, error) {
RastaStyle,
MetaStyle,
SquareStyle,
NeonStyle, // 霓虹风格
PixelStyle, // 像素风格
WatercolorStyle, // 水彩风格
MechStyle, // 机械风格
CosmicStyle, // 宇宙风格
GhostStyle, // 幽灵风格
} {
if s == style {
return i, nil

14
style/watercolor_style.go Normal file
View File

@@ -0,0 +1,14 @@
package style
// WatercolorStyle 水彩风格类型
const WatercolorStyle StyleType = "watercolor"
// WatercolorStyleShapes 水彩风格形状集合
var WatercolorStyleShapes = StyleSet{
TypeClo: "<path id='clo' d=\"m141.75 195c4.11 1.1 7.96 2.31 11.52 3.6 8.47 4.73 16.22 9.77 22.52 15.11 2.47 1.58 4.67 3.23 6.58 4.93-9.89 7.44-21.04 13.65-33.05 18.33-19.46 6.15-41.01 9.53-63.33 9.53-22.32 0-43.87-3.38-63.33-9.53-12.01-4.68-23.16-10.89-33.05-18.33 1.91-1.7 4.11-3.35 6.58-4.93 6.3-5.34 14.05-10.38 22.52-15.11 3.56-1.29 7.41-2.5 11.52-3.6 1.61 4.09 7.22 16.21 20.88 22.8 14.2 4.45 23.57 3.17 34.87 3.17 11.3 0 20.67 1.28 34.87-3.17 13.66-6.59 19.27-18.71 20.88-22.8z\" style=\"fill:#f7f7f7;stroke-width:1px;stroke:#ddd;\"/>",
TypeMouth: "<path id='mouth' d=\"m115.5 155c-5 0-10-2.5-10-5s5-5 10-5 10 2.5 10 5-5 5-10 5z\" style=\"fill:#f07;stroke-linecap:round;stroke-linejoin:round;stroke-width:1px;stroke:#f07;\"/>",
TypeEyes: "<path id='eyes' d=\"m85 105c-3.866 0-7-3.134-7-7s3.134-7 7-7 7 3.134 7 7-3.134 7-7 7zm60 0c-3.866 0-7-3.134-7-7s3.134-7 7-7 7 3.134 7 7-3.134 7-7 7z\" style=\"fill:#40a;stroke-linecap:round;stroke-linejoin:round;stroke-width:1px;stroke:#40a;\"/>",
TypeTop: "<path id='top' d=\"m65.5 60c-5 5-10 10-10 20 0 5 0 15 5 20 5-15 15-25 55-25s50 10 55 25c5-5 5-15 5-20 0-10-5-15-10-20-20-10-80-10-100 0z\" style=\"fill:#ddd;stroke-linecap:round;stroke-linejoin:round;stroke-width:1px;stroke:#ccc;\"/>",
TypeHead: "<path id='head' d=\"m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z\" style=\"fill:#000;\"/>",
TypeEnv: "<path id='env' d=\"M33.83,33.83a115.5,115.5,0,1,1,0,163.34,115.49,115.49,0,0,1,0-163.34Z\" style=\"fill:#01;\"/>",
}

32
theme/cosmic_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// CosmicTheme 宇宙风格主题
var CosmicTheme = Theme{
// 第一部分 - 星空风格
ThemePart{
"env": {"000033"},
"clo": {"000000", "8426c7"},
"head": {"000000"},
"mouth": {"ae00ff"},
"eyes": {"5500ff", "00ffff"},
"top": {"000000", "8426c7"},
},
// 第二部分 - 银河风格
ThemePart{
"env": {"0a001a"},
"clo": {"000000", "00ffaa"},
"head": {"000000"},
"mouth": {"00ffaa"},
"eyes": {"00aaff", "ffffff"},
"top": {"000000", "00ffaa"},
},
// 第三部分 - 星云风格
ThemePart{
"env": {"330033"},
"clo": {"000000", "ff00ff"},
"head": {"000000"},
"mouth": {"ff00ff"},
"eyes": {"ff00aa", "ffffff"},
"top": {"000000", "ff00ff"},
},
}

32
theme/ghost_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// GhostTheme 幽灵风格主题
var GhostTheme = Theme{
// 第一部分 - 经典幽灵风格
ThemePart{
"env": {"1a1a1a"},
"clo": {"ffffff", "ffffff"},
"head": {"ffffff"},
"mouth": {"000000"},
"eyes": {"000000"},
"top": {"ffffff"},
},
// 第二部分 - 蓝色幽灵风格
ThemePart{
"env": {"000033"},
"clo": {"aaddff", "aaddff"},
"head": {"aaddff"},
"mouth": {"000000"},
"eyes": {"000000"},
"top": {"aaddff"},
},
// 第三部分 - 绿色幽灵风格
ThemePart{
"env": {"002211"},
"clo": {"aaeecc", "aaeecc"},
"head": {"aaeecc"},
"mouth": {"000000"},
"eyes": {"000000"},
"top": {"aaeecc"},
},
}

View File

@@ -484,6 +484,24 @@ var defaultThemeSet = map[style.StyleType]Theme{
"top": {"#00fffd", "none", "none", "none", "none"},
},
},
// 添加NeonStyle主题
style.NeonStyle: NeonTheme,
// 添加PixelStyle主题
style.PixelStyle: PixelTheme,
// 添加WatercolorStyle主题
style.WatercolorStyle: WatercolorTheme,
// 添加MechStyle主题
style.MechStyle: MechTheme,
// 添加CosmicStyle主题
style.CosmicStyle: CosmicTheme,
// 添加GhostStyle主题
style.GhostStyle: GhostTheme,
}
// initThemes 初始化主题数据
@@ -507,6 +525,12 @@ func (m *Manager) initThemes() {
style.RastaStyle,
style.MetaStyle,
style.SquareStyle,
style.NeonStyle, // 霓虹风格主题
style.PixelStyle, // 像素风格主题
style.WatercolorStyle, // 水彩风格主题
style.MechStyle, // 机械风格主题
style.CosmicStyle, // 宇宙风格主题
style.GhostStyle, // 幽灵风格主题
} {
if themeSet, exists := defaultThemeSet[theme]; exists {
m.AddTheme(themeSet)

32
theme/mech_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// MechTheme 机械风格主题
var MechTheme = Theme{
// 第一部分 - 钢铁机械风格
ThemePart{
"env": {"333333"},
"clo": {"555555", "777777", "333333"},
"head": {"888888"},
"mouth": {"222222", "555555"},
"eyes": {"333333", "73e8ff"},
"top": {"444444", "777777", "222222", "555555"},
},
// 第二部分 - 铜色机械风格
ThemePart{
"env": {"251607"},
"clo": {"805723", "ab752f", "553211"},
"head": {"a87e45"},
"mouth": {"302013", "604020"},
"eyes": {"553211", "73e8ff"},
"top": {"805723", "ab752f", "302013", "604020"},
},
// 第三部分 - 未来机械风格
ThemePart{
"env": {"002244"},
"clo": {"1a1a1a", "333333", "007acc"},
"head": {"444444"},
"mouth": {"1a1a1a", "007acc"},
"eyes": {"1a1a1a", "00ffff"},
"top": {"1a1a1a", "333333", "007acc", "00aaff"},
},
}

32
theme/neon_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// NeonTheme 霓虹风格主题
var NeonTheme = Theme{
// 第一部分 - 赛博朋克风格
ThemePart{
"env": {"000000"},
"clo": {"1e1e1e", "5c5c5c", "00f2ff"},
"head": {"000000"},
"mouth": {"ff00d4", "00f2ff"},
"eyes": {"ff00d4", "00f2ff", "ffffff", "ffffff"},
"top": {"1e1e1e", "00f2ff", "ff00d4"},
},
// 第二部分 - 霓虹黄风格
ThemePart{
"env": {"0d0030"},
"clo": {"1e1e1e", "5c5c5c", "ffdd00"},
"head": {"000000"},
"mouth": {"ffdd00", "00f2ff"},
"eyes": {"ffdd00", "00f2ff", "ffffff", "ffffff"},
"top": {"1e1e1e", "00f2ff", "ffdd00"},
},
// 第三部分 - 霓虹绿风格
ThemePart{
"env": {"100a00"},
"clo": {"1e1e1e", "5c5c5c", "0cff6f"},
"head": {"000000"},
"mouth": {"0cff6f", "ff00d4"},
"eyes": {"0cff6f", "ff00d4", "ffffff", "ffffff"},
"top": {"1e1e1e", "ff00d4", "0cff6f"},
},
}

32
theme/pixel_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// PixelTheme 像素风格主题
var PixelTheme = Theme{
// 第一部分 - 经典像素风格
ThemePart{
"env": {"333333"},
"clo": {"3F51B5"},
"head": {"ffcc99"},
"mouth": {"cc0000"},
"eyes": {"000000"},
"top": {"663300"},
},
// 第二部分 - 像素游戏风格
ThemePart{
"env": {"4CAF50"},
"clo": {"607D8B"},
"head": {"ffbb77"},
"mouth": {"333333"},
"eyes": {"333333"},
"top": {"ff9900"},
},
// 第三部分 - 复古像素风格
ThemePart{
"env": {"C62828"},
"clo": {"9C27B0"},
"head": {"CDDC39"},
"mouth": {"000000"},
"eyes": {"000000"},
"top": {"FF5722"},
},
}

32
theme/watercolor_theme.go Normal file
View File

@@ -0,0 +1,32 @@
package theme
// WatercolorTheme 水彩风格主题
var WatercolorTheme = Theme{
// 第一部分 - 粉色水彩风格
ThemePart{
"env": {"ffeeff"},
"clo": {"f7f7f7", "dddddd"},
"head": {"fff0e8"},
"mouth": {"ff5599", "ff5599"},
"eyes": {"5544aa", "5544aa"},
"top": {"dddddd", "cccccc"},
},
// 第二部分 - 蓝色水彩风格
ThemePart{
"env": {"eeffff"},
"clo": {"f7f7f7", "dddddd"},
"head": {"fff0f0"},
"mouth": {"00aabb", "00aabb"},
"eyes": {"3355dd", "3355dd"},
"top": {"ccddff", "bbccee"},
},
// 第三部分 - 绿色水彩风格
ThemePart{
"env": {"eeffee"},
"clo": {"f7f7f7", "dddddd"},
"head": {"fffcf0"},
"mouth": {"66aa66", "66aa66"},
"eyes": {"006600", "006600"},
"top": {"ddffdd", "cceecc"},
},
}