feat: 添加基础图形处理模块,包括精灵管理、绘制功能和图像渲染支持。

This commit is contained in:
lixiangwuxian 2025-05-10 01:14:56 +08:00
parent c03d1c20b8
commit e3149f6079
6 changed files with 1189 additions and 0 deletions

28
draw/draw.go Normal file
View File

@ -0,0 +1,28 @@
package draw
import (
"image"
"image/draw"
)
type Drawer interface {
Draw(img *image.Image)
}
type BaseBoard struct {
Image *image.RGBA64
}
func (b *BaseBoard) SetBaseImage(img *image.Image) {
b.Image = ToRGBA64(img)
}
func (b *BaseBoard) Draw(img *image.Image, pos image.Point) {
draw.Draw(b.Image, b.Image.Bounds(), *img, pos, draw.Over)
}
func ToRGBA64(img *image.Image) *image.RGBA64 {
rgba := image.NewRGBA64((*img).Bounds())
draw.Draw(rgba, rgba.Bounds(), *img, image.Point{}, draw.Over)
return rgba
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.lxtend.com/lixiangwuxian/imagedd
go 1.24.2

41
model/2d.go Normal file
View File

@ -0,0 +1,41 @@
package model
import (
"image"
"image/color"
"image/draw"
)
type Line struct {
Start image.Point
End image.Point
Width int
Color color.Color
}
func (l *Line) ToSprite() *Sprite {
img := image.NewRGBA(image.Rect(0, 0, l.End.X-l.Start.X, l.End.Y-l.Start.Y))
draw.Draw(img, image.Rect(0, 0, l.End.X-l.Start.X, l.End.Y-l.Start.Y), &image.Uniform{l.Color}, image.Point{}, draw.Src)
return &Sprite{
Position: l.Start,
Image: img,
}
}
type ProjectMatrix struct {
Matrix [2][2]float64
}
func (p *ProjectMatrix) ProjectPoint(point image.Point) image.Point {
return image.Point{
X: int(p.Matrix[0][0]*float64(point.X) + p.Matrix[0][1]*float64(point.Y)),
Y: int(p.Matrix[1][0]*float64(point.X) + p.Matrix[1][1]*float64(point.Y)),
}
}
func (p *ProjectMatrix) ProjectLine(line Line) Line {
return Line{
Start: p.ProjectPoint(line.Start),
End: p.ProjectPoint(line.End),
}
}

522
model/named_sprite_board.go Normal file
View File

@ -0,0 +1,522 @@
package model
import (
"image"
"image/draw"
"image/png"
"log"
"os"
"sort"
)
// 设置为true启用调试日志
var debugLog = false
func logDebug(format string, args ...interface{}) {
if debugLog {
log.Printf(format, args...)
}
}
// SpriteIndexNode 表示同一个Index下的精灵链表节点
type SpriteIndexNode struct {
Sprite *Sprite
Next *SpriteIndexNode // 指向同一Index下的下一个精灵
}
// IndexGroup 表示一个索引组包含所有具有相同Index的精灵
type IndexGroup struct {
Index int
Head *SpriteIndexNode // 指向第一个精灵
Count int // 该组中精灵数量
Next *IndexGroup // 指向下一个索引组
Prev *IndexGroup // 指向上一个索引组
}
// NamedSpriteBoard 提供按名称哈希查找,按索引排序的精灵图管理
type NamedSpriteBoard struct {
Width int
Height int
Sprites []*Sprite
// nameMap 用于O(1)的按名称查找键为Name值为精灵指针
nameMap map[string]*Sprite
// indexGroups 按Index排序的索引组链表
indexHead *IndexGroup // 索引组链表头
indexTail *IndexGroup // 索引组链表尾
// 索引组的映射表用于O(1)按索引查找索引组
indexMap map[int]*IndexGroup
// 总精灵数量
count int
}
// NewNamedSpriteBoard 创建一个新的NamedSpriteBoard
func NewNamedSpriteBoard(width, height int) *NamedSpriteBoard {
return &NamedSpriteBoard{
Width: width,
Height: height,
Sprites: make([]*Sprite, 0),
nameMap: make(map[string]*Sprite),
indexMap: make(map[int]*IndexGroup),
indexHead: nil,
indexTail: nil,
count: 0,
}
}
// AddSprite 添加精灵O(1)时间复杂度查找O(logn)插入
func (b *NamedSpriteBoard) AddSprite(sprite *Sprite) {
// 必须有名称
if sprite.Name == "" {
return
}
// 如果已存在同名精灵,先移除
if _, exists := b.nameMap[sprite.Name]; exists {
b.RemoveSpriteByName(sprite.Name)
}
// 将精灵添加到名称映射
b.nameMap[sprite.Name] = sprite
// 查找或创建索引组
indexGroup, exists := b.indexMap[sprite.Index]
if !exists {
// 创建新的索引组
indexGroup = &IndexGroup{
Index: sprite.Index,
Head: nil,
Count: 0,
Next: nil,
Prev: nil,
}
// 将索引组添加到映射表
b.indexMap[sprite.Index] = indexGroup
// 插入到索引组链表中的正确位置
b.insertIndexGroup(indexGroup)
}
// 创建新的精灵节点
newNode := &SpriteIndexNode{
Sprite: sprite,
Next: nil,
}
// 在索引组中按名称排序插入
b.insertSpriteToGroup(indexGroup, newNode)
// 更新计数
indexGroup.Count++
b.count++
// 更新兼容层
b.updateSpriteArray()
}
// insertIndexGroup 将索引组插入到正确的位置O(logn)时间复杂度
func (b *NamedSpriteBoard) insertIndexGroup(group *IndexGroup) {
// 如果是第一个索引组
if b.indexHead == nil {
b.indexHead = group
b.indexTail = group
return
}
// 如果应该插入到头部
if group.Index < b.indexHead.Index {
group.Next = b.indexHead
b.indexHead.Prev = group
b.indexHead = group
return
}
// 如果应该插入到尾部
if group.Index > b.indexTail.Index {
group.Prev = b.indexTail
b.indexTail.Next = group
b.indexTail = group
return
}
// 找到插入位置 - 将链表的元素放入数组进行二分查找
groups := make([]*IndexGroup, 0)
current := b.indexHead
for current != nil {
groups = append(groups, current)
current = current.Next
}
// 二分查找插入位置
insertPos := sort.Search(len(groups), func(i int) bool {
return groups[i].Index >= group.Index
})
// 插入到找到的位置之前
prevGroup := groups[insertPos-1]
nextGroup := groups[insertPos]
group.Next = nextGroup
group.Prev = prevGroup
prevGroup.Next = group
nextGroup.Prev = group
}
// insertSpriteToGroup 将精灵节点按名称排序插入到索引组O(n)时间复杂度
// 由于同一个Index下的精灵数量通常不多所以使用简单的插入排序
func (b *NamedSpriteBoard) insertSpriteToGroup(group *IndexGroup, node *SpriteIndexNode) {
// 如果索引组为空
if group.Head == nil {
group.Head = node
return
}
// 如果应该插入到头部
if node.Sprite.Name < group.Head.Sprite.Name {
node.Next = group.Head
group.Head = node
return
}
// 查找插入位置
current := group.Head
for current.Next != nil && current.Next.Sprite.Name < node.Sprite.Name {
current = current.Next
}
// 插入节点
node.Next = current.Next
current.Next = node
}
// GetSpriteByName 通过名称获取精灵O(1)时间复杂度
func (b *NamedSpriteBoard) GetSpriteByName(name string) *Sprite {
sprite, exists := b.nameMap[name]
if exists {
return sprite
}
return nil
}
// GetSpritesByIndex 获取具有特定索引的所有精灵O(1)查找 + O(n)收集
func (b *NamedSpriteBoard) GetSpritesByIndex(index int) []*Sprite {
group, exists := b.indexMap[index]
if !exists {
return nil
}
sprites := make([]*Sprite, 0, group.Count)
current := group.Head
for current != nil {
sprites = append(sprites, current.Sprite)
current = current.Next
}
return sprites
}
// RemoveSpriteByName 通过名称删除精灵O(1)查找 + O(n)删除
func (b *NamedSpriteBoard) RemoveSpriteByName(name string) bool {
sprite, exists := b.nameMap[name]
if !exists {
return false
}
// 从名称映射中移除
delete(b.nameMap, name)
// 查找对应的索引组
group := b.indexMap[sprite.Index]
if group == nil {
return false
}
// 从索引组中移除精灵
removed := b.removeSpriteFromGroup(group, name)
if !removed {
return false
}
// 如果索引组变为空,则删除该索引组
if group.Count == 0 {
b.removeIndexGroup(group)
}
// 更新计数
b.count--
// 更新兼容层
b.updateSpriteArray()
return true
}
// removeSpriteFromGroup 从索引组中移除指定名称的精灵O(n)时间复杂度
func (b *NamedSpriteBoard) removeSpriteFromGroup(group *IndexGroup, name string) bool {
logDebug("removeSpriteFromGroup: 尝试从索引组 %d 中移除精灵 %s", group.Index, name)
// 如果索引组为空
if group.Head == nil {
logDebug("removeSpriteFromGroup: 索引组为空")
return false
}
// 如果是头节点
if group.Head.Sprite.Name == name {
logDebug("removeSpriteFromGroup: 移除头节点 %s", name)
group.Head = group.Head.Next
group.Count--
return true
}
// 查找节点
current := group.Head
for current.Next != nil && current.Next.Sprite.Name != name {
logDebug("removeSpriteFromGroup: 检查节点 %s", current.Next.Sprite.Name)
current = current.Next
}
// 如果找到了节点
if current.Next != nil {
logDebug("removeSpriteFromGroup: 找到并移除节点 %s", name)
current.Next = current.Next.Next
group.Count--
return true
}
logDebug("removeSpriteFromGroup: 未找到节点 %s", name)
return false
}
// removeIndexGroup 从索引组链表中移除索引组O(1)时间复杂度
func (b *NamedSpriteBoard) removeIndexGroup(group *IndexGroup) {
// 从索引映射表中移除
delete(b.indexMap, group.Index)
// 从链表中移除
if group.Prev != nil {
group.Prev.Next = group.Next
} else {
b.indexHead = group.Next // 头节点变更
}
if group.Next != nil {
group.Next.Prev = group.Prev
} else {
b.indexTail = group.Prev // 尾节点变更
}
}
// UpdateSpriteIndex 更新精灵的索引O(1)查找 + O(logn)插入
func (b *NamedSpriteBoard) UpdateSpriteIndex(name string, newIndex int) bool {
sprite, exists := b.nameMap[name]
if !exists {
return false
}
// 保存旧索引
oldIndex := sprite.Index
// 从旧索引组中移除
oldGroup := b.indexMap[oldIndex]
if oldGroup == nil {
return false
}
if !b.removeSpriteFromGroup(oldGroup, name) {
return false
}
// 如果旧索引组变为空,则删除
if oldGroup.Count == 0 {
b.removeIndexGroup(oldGroup)
}
// 更新精灵的索引
sprite.Index = newIndex
// 查找或创建新索引组
newGroup, exists := b.indexMap[newIndex]
if !exists {
// 创建新的索引组
newGroup = &IndexGroup{
Index: newIndex,
Head: nil,
Count: 0,
Next: nil,
Prev: nil,
}
// 将索引组添加到映射表
b.indexMap[newIndex] = newGroup
// 插入到索引组链表中的正确位置
b.insertIndexGroup(newGroup)
}
// 创建新的精灵节点
newNode := &SpriteIndexNode{
Sprite: sprite,
Next: nil,
}
// 在新索引组中按名称排序插入
b.insertSpriteToGroup(newGroup, newNode)
newGroup.Count++
// 更新兼容层
b.updateSpriteArray()
return true
}
// UpdateSpriteName 更新精灵的名称O(1)查找 + O(n)插入
func (b *NamedSpriteBoard) UpdateSpriteName(oldName, newName string) bool {
logDebug("UpdateSpriteName: 尝试将 %s 更新为 %s", oldName, newName)
sprite, exists := b.nameMap[oldName]
if !exists {
logDebug("UpdateSpriteName: 未找到精灵 %s", oldName)
return false
}
// 检查新名称是否已存在
if _, exists := b.nameMap[newName]; exists && newName != oldName {
logDebug("UpdateSpriteName: 新名称 %s 已存在", newName)
return false
}
// 保存索引
index := sprite.Index
logDebug("UpdateSpriteName: 精灵索引为 %d", index)
// 从索引组中移除再添加,以保持排序
group := b.indexMap[index]
if group == nil {
logDebug("UpdateSpriteName: 未找到索引组 %d", index)
return false
}
logDebug("UpdateSpriteName: 在索引组 %d 中查找名称 %s", index, oldName)
// 调试输出当前索引组的所有精灵
current := group.Head
i := 0
for current != nil {
logDebug("UpdateSpriteName: 索引组精灵[%d]: %s", i, current.Sprite.Name)
current = current.Next
i++
}
// 从索引组链表中删除节点
if !b.removeSpriteFromGroup(group, oldName) {
logDebug("UpdateSpriteName: 从索引组中移除精灵 %s 失败", oldName)
return false
}
// 从名称映射中移除旧名称
delete(b.nameMap, oldName)
// 更新精灵名称
sprite.Name = newName
// 添加到新名称映射
b.nameMap[newName] = sprite
// 创建新节点并添加回索引组
newNode := &SpriteIndexNode{
Sprite: sprite,
Next: nil,
}
b.insertSpriteToGroup(group, newNode)
group.Count++ // 增加计数因为removeSpriteFromGroup已经减少了计数
// 更新兼容层
b.updateSpriteArray()
logDebug("UpdateSpriteName: 成功更新名称从 %s 到 %s", oldName, newName)
return true
}
// GetAllSprites 获取所有精灵按Index主排序Name次排序
func (b *NamedSpriteBoard) GetAllSprites() []*Sprite {
sprites := make([]*Sprite, 0, b.count)
// 遍历所有索引组
currentGroup := b.indexHead
for currentGroup != nil {
// 遍历当前索引组中的所有精灵
currentNode := currentGroup.Head
for currentNode != nil {
sprites = append(sprites, currentNode.Sprite)
currentNode = currentNode.Next
}
currentGroup = currentGroup.Next
}
return sprites
}
// updateSpriteArray 更新BaseBoard中的Sprites数组以保持兼容性
func (b *NamedSpriteBoard) updateSpriteArray() {
b.Sprites = b.GetAllSprites()
}
// RenderToImage 将精灵板渲染为图像
func (b *NamedSpriteBoard) RenderToImage() *image.RGBA {
// 创建画布
img := image.NewRGBA(image.Rect(0, 0, b.Width, b.Height))
// 按索引排序(已经在链表中排好)遍历所有精灵组
currentGroup := b.indexHead
for currentGroup != nil {
// 在每个索引组中,按名称排序(已经在链表中排好)遍历所有精灵
currentNode := currentGroup.Head
for currentNode != nil {
sprite := currentNode.Sprite
if sprite.Image != nil {
// 获取精灵的边界
bounds := sprite.Image.Bounds()
// 计算目标区域
min := image.Point{
X: sprite.Position.X,
Y: sprite.Position.Y,
}
max := image.Point{
X: sprite.Position.X + bounds.Dx(),
Y: sprite.Position.Y + bounds.Dy(),
}
// 绘制精灵到画布上
draw.Draw(img, image.Rectangle{Min: min, Max: max}, sprite.Image, bounds.Min, draw.Over)
}
// 移动到下一个精灵
currentNode = currentNode.Next
}
// 移动到下一个索引组
currentGroup = currentGroup.Next
}
return img
}
// SaveToFile 将精灵板渲染为图像并保存到文件
func (b *NamedSpriteBoard) SaveToFile(filename string) error {
// 渲染图像
img := b.RenderToImage()
// 创建文件
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
// 将图像编码为PNG并写入文件
if err := png.Encode(f, img); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,387 @@
package model
import (
"image"
"image/color"
"strconv"
"testing"
)
func TestNamedSpriteBoard(t *testing.T) {
// 创建测试板
board := NewNamedSpriteBoard(100, 100)
// 测试添加精灵
sprite1 := &Sprite{
Name: "sprite1",
Position: image.Point{X: 10, Y: 10},
Image: image.NewRGBA(image.Rect(0, 0, 10, 10)),
Index: 5,
}
sprite2 := &Sprite{
Name: "sprite2",
Position: image.Point{X: 20, Y: 20},
Image: image.NewRGBA(image.Rect(0, 0, 10, 10)),
Index: 3,
}
sprite3 := &Sprite{
Name: "sprite3",
Position: image.Point{X: 30, Y: 30},
Image: image.NewRGBA(image.Rect(0, 0, 10, 10)),
Index: 8,
}
// 添加精灵
board.AddSprite(sprite1)
board.AddSprite(sprite2)
board.AddSprite(sprite3)
// 验证总数量
if board.count != 3 {
t.Errorf("期望精灵总数为3实际为%d", board.count)
}
// 验证索引组数量
if len(board.indexMap) != 3 {
t.Errorf("期望索引组数量为3实际为%d", len(board.indexMap))
}
// 验证索引组链表顺序
expectedIndices := []int{3, 5, 8}
currentGroup := board.indexHead
for i, expectedIndex := range expectedIndices {
if currentGroup == nil {
t.Errorf("索引组链表在位置%d处意外结束", i)
break
}
if currentGroup.Index != expectedIndex {
t.Errorf("索引组链表顺序错误,位置%d期望为%d实际为%d", i, expectedIndex, currentGroup.Index)
}
currentGroup = currentGroup.Next
}
// 验证兼容层
if len(board.Sprites) != 3 {
t.Errorf("兼容层数组长度错误期望为3实际为%d", len(board.Sprites))
}
for i, expectedIndex := range expectedIndices {
if board.Sprites[i].Index != expectedIndex {
t.Errorf("兼容层数组索引顺序错误,位置%d期望为%d实际为%d", i, expectedIndex, board.Sprites[i].Index)
}
}
// 测试按名称查找
foundSprite := board.GetSpriteByName("sprite1")
if foundSprite == nil || foundSprite.Index != 5 {
t.Errorf("未能按名称找到精灵sprite1")
}
// 测试按索引查找
sprites := board.GetSpritesByIndex(5)
if len(sprites) != 1 || sprites[0].Name != "sprite1" {
t.Errorf("按索引查找精灵错误")
}
// 添加相同索引的精灵
sprite4 := &Sprite{
Name: "sprite4",
Position: image.Point{X: 40, Y: 40},
Image: image.NewRGBA(image.Rect(0, 0, 10, 10)),
Index: 5, // 与sprite1相同的索引
}
board.AddSprite(sprite4)
// 验证总数量增加
if board.count != 4 {
t.Errorf("添加相同索引精灵后期望精灵总数为4实际为%d", board.count)
}
// 验证索引组数量不变
if len(board.indexMap) != 3 {
t.Errorf("添加相同索引精灵后期望索引组数量为3实际为%d", len(board.indexMap))
}
// 验证索引5组内有两个精灵且按名称排序
index5Group := board.indexMap[5]
if index5Group.Count != 2 {
t.Errorf("索引5组内精灵数量错误期望为2实际为%d", index5Group.Count)
}
// 验证索引5组内精灵按名称排序sprite1应在sprite4之前
if index5Group.Head.Sprite.Name != "sprite1" || index5Group.Head.Next.Sprite.Name != "sprite4" {
t.Errorf("索引5组内精灵顺序错误期望为sprite1, sprite4实际为%s, %s",
index5Group.Head.Sprite.Name, index5Group.Head.Next.Sprite.Name)
}
// 验证按索引5查找返回两个精灵
sprites = board.GetSpritesByIndex(5)
if len(sprites) != 2 {
t.Errorf("按索引5查找应返回2个精灵实际返回%d个", len(sprites))
}
// 测试删除精灵
success := board.RemoveSpriteByName("sprite1")
if !success {
t.Errorf("删除精灵sprite1失败")
}
// 验证总数量减少
if board.count != 3 {
t.Errorf("删除后期望精灵总数为3实际为%d", board.count)
}
// 验证无法查找到已删除的精灵
foundSprite = board.GetSpriteByName("sprite1")
if foundSprite != nil {
t.Errorf("已删除的精灵仍可被查找到")
}
// 验证索引5组仍然存在且只有一个精灵
index5Group = board.indexMap[5]
if index5Group == nil || index5Group.Count != 1 || index5Group.Head.Sprite.Name != "sprite4" {
t.Errorf("删除sprite1后索引5组状态错误")
}
// 测试更新精灵索引
success = board.UpdateSpriteIndex("sprite4", 10)
if !success {
t.Errorf("更新精灵索引失败")
}
// 验证索引已更新
foundSprite = board.GetSpriteByName("sprite4")
if foundSprite == nil || foundSprite.Index != 10 {
t.Errorf("精灵索引未成功更新")
}
// 验证原索引组被删除,新索引组已创建
_, exists := board.indexMap[5]
if exists {
t.Errorf("原索引5组未被删除")
}
_, exists = board.indexMap[10]
if !exists {
t.Errorf("新索引10组未被创建")
}
// 测试更新精灵名称
t.Logf("测试更新名称,当前精灵:%+v", *board.GetSpriteByName("sprite4"))
success = board.UpdateSpriteName("sprite4", "sprite5")
if !success {
t.Errorf("更新精灵名称失败")
}
// 验证名称已更新
foundSprite = board.GetSpriteByName("sprite5")
if foundSprite == nil {
t.Errorf("无法用新名称找到精灵")
}
foundSprite = board.GetSpriteByName("sprite4")
if foundSprite != nil {
t.Errorf("仍能用旧名称找到精灵")
}
}
// 性能测试
func BenchmarkNamedSpriteBoardAdd(b *testing.B) {
board := NewNamedSpriteBoard(1000, 1000)
img := image.NewRGBA(image.Rect(0, 0, 10, 10))
b.ResetTimer()
for i := 0; i < b.N; i++ {
sprite := &Sprite{
Name: "sprite" + strconv.Itoa(i),
Position: image.Point{X: i % 100, Y: i / 100},
Image: img,
Index: i % 100, // 使用100个不同的索引模拟多个精灵共享索引
}
board.AddSprite(sprite)
}
}
func BenchmarkNamedSpriteBoardGetByName(b *testing.B) {
// 准备测试数据
board := NewNamedSpriteBoard(1000, 1000)
img := image.NewRGBA(image.Rect(0, 0, 10, 10))
names := make([]string, 1000)
for i := 0; i < 1000; i++ {
name := "sprite" + strconv.Itoa(i)
names[i] = name
sprite := &Sprite{
Name: name,
Position: image.Point{X: i % 100, Y: i / 100},
Image: img,
Index: i % 100,
}
board.AddSprite(sprite)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
board.GetSpriteByName(names[i%1000])
}
}
func BenchmarkNamedSpriteBoardGetByIndex(b *testing.B) {
// 准备测试数据
board := NewNamedSpriteBoard(1000, 1000)
img := image.NewRGBA(image.Rect(0, 0, 10, 10))
for i := 0; i < 1000; i++ {
sprite := &Sprite{
Name: "sprite" + strconv.Itoa(i),
Position: image.Point{X: i % 100, Y: i / 100},
Image: img,
Index: i % 100,
}
board.AddSprite(sprite)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
board.GetSpritesByIndex(i % 100)
}
}
func TestNamedSpriteBoardRenderToImage(t *testing.T) {
// 创建测试板
board := NewNamedSpriteBoard(200, 200)
// 创建几个具有不同索引的彩色精灵
// 索引值小的精灵会被先绘制,索引值大的精灵会覆盖在上面
// 创建红色精灵索引1会被绘制在最底层
redImg := image.NewRGBA(image.Rect(0, 0, 50, 50))
for y := 0; y < 50; y++ {
for x := 0; x < 50; x++ {
redImg.Set(x, y, color.RGBA{255, 0, 0, 255}) // 红色
}
}
redSprite := &Sprite{
Name: "red",
Position: image.Point{X: 50, Y: 50},
Image: redImg,
Index: 1,
}
// 创建绿色精灵索引2会覆盖红色精灵
greenImg := image.NewRGBA(image.Rect(0, 0, 50, 50))
for y := 0; y < 50; y++ {
for x := 0; x < 50; x++ {
greenImg.Set(x, y, color.RGBA{0, 255, 0, 255}) // 绿色
}
}
greenSprite := &Sprite{
Name: "green",
Position: image.Point{X: 75, Y: 75},
Image: greenImg,
Index: 2,
}
// 创建蓝色精灵索引2与绿色相同索引按名称排序在绿色之前
blueImg := image.NewRGBA(image.Rect(0, 0, 50, 50))
for y := 0; y < 50; y++ {
for x := 0; x < 50; x++ {
blueImg.Set(x, y, color.RGBA{0, 0, 255, 255}) // 蓝色
}
}
blueSprite := &Sprite{
Name: "blue",
Position: image.Point{X: 85, Y: 85},
Image: blueImg,
Index: 2,
}
// 将精灵添加到精灵板添加顺序混乱但应按index和name排序渲染
board.AddSprite(greenSprite)
board.AddSprite(redSprite)
board.AddSprite(blueSprite)
// 验证精灵被正确添加
if board.count != 3 {
t.Errorf("期望精灵数量为3实际为%d", board.count)
}
// 验证索引组数量
if len(board.indexMap) != 2 {
t.Errorf("期望索引组数量为2实际为%d", len(board.indexMap))
}
// 验证索引为2的组内有两个精灵
index2Group := board.indexMap[2]
if index2Group.Count != 2 {
t.Errorf("索引2组内精灵数量错误期望为2实际为%d", index2Group.Count)
}
// 验证索引为2的组内精灵按名称排序blue应在green之前
if index2Group.Head.Sprite.Name != "blue" || index2Group.Head.Next.Sprite.Name != "green" {
t.Errorf("索引2组内精灵顺序错误期望为blue, green实际为%s, %s",
index2Group.Head.Sprite.Name, index2Group.Head.Next.Sprite.Name)
}
// 渲染图像
img := board.RenderToImage()
// 验证图像大小
if img.Bounds().Dx() != 200 || img.Bounds().Dy() != 200 {
t.Errorf("渲染图像大小错误期望200x200实际%dx%d", img.Bounds().Dx(), img.Bounds().Dy())
}
// 将saveImage设置为true查看实际渲染效果以调试
saveImage := false
if saveImage {
err := board.SaveToFile("test_render.png")
if err != nil {
t.Errorf("保存图像失败: %v", err)
}
t.Log("图像已保存至 test_render.png")
}
// 检查渲染顺序:
// 索引排序:索引小的先渲染,后渲染的会覆盖先渲染的
// 在同一索引中按名称字母顺序排序blue在green之前渲染
// 检查红色精灵位置索引1最小最底层
redPos := image.Point{X: 60, Y: 60}
redColor := img.At(redPos.X, redPos.Y)
r, g, b, _ := redColor.RGBA()
if r>>8 != 255 || g>>8 != 0 || b>>8 != 0 {
t.Errorf("位置(%d,%d)颜色错误,期望为红色(255,0,0),实际为(%d,%d,%d)",
redPos.X, redPos.Y, r>>8, g>>8, b>>8)
}
// 检查绿色特有区域
greenPos := image.Point{X: 120, Y: 120}
greenColor := img.At(greenPos.X, greenPos.Y)
r, g, b, _ = greenColor.RGBA()
if r>>8 != 0 || g>>8 != 255 || b>>8 != 0 {
t.Errorf("位置(%d,%d)颜色错误,期望为绿色(0,255,0),实际为(%d,%d,%d)",
greenPos.X, greenPos.Y, r>>8, g>>8, b>>8)
}
// 检查蓝色特有区域
bluePos := image.Point{X: 130, Y: 130}
blueColor := img.At(bluePos.X, bluePos.Y)
r, g, b, _ = blueColor.RGBA()
if r>>8 != 0 || g>>8 != 0 || b>>8 != 255 {
t.Errorf("位置(%d,%d)颜色错误,期望为蓝色(0,0,255),实际为(%d,%d,%d)",
bluePos.X, bluePos.Y, r>>8, g>>8, b>>8)
}
// 检查重叠区域(按实际绘制顺序)
// 记住绘制的顺序red(索引1) -> blue(索引2) -> green(索引2)
// 所以重叠区域应该是最后绘制的颜色
overlapPos := image.Point{X: 90, Y: 90}
overlapColor := img.At(overlapPos.X, overlapPos.Y)
r, g, b, _ = overlapColor.RGBA()
// 即使blue按名称排序在前b < g但按添加顺序渲染后green会覆盖blue
// 按正确的排序,索引大的排后面,同索引按名称排序
if r>>8 != 0 || g>>8 != 255 || b>>8 != 0 {
t.Errorf("重叠位置(%d,%d)颜色错误,期望为绿色(0,255,0),实际为(%d,%d,%d)",
overlapPos.X, overlapPos.Y, r>>8, g>>8, b>>8)
}
}

208
model/sprite.go Normal file
View File

@ -0,0 +1,208 @@
package model
import (
"image"
"math"
)
type Sprite struct {
Name string
Position image.Point
Image image.Image
Index int
}
func (s *Sprite) SetImage(img *image.RGBA) {
s.Image = img
}
func (s *Sprite) Rotate(angle float64) {
// 创建旋转矩阵
sin, cos := math.Sin(angle), math.Cos(angle)
// 获取原始图像的边界
bounds := s.Image.Bounds()
width, height := bounds.Dx(), bounds.Dy()
centerX, centerY := width/2, height/2
// 计算旋转后图像的边界
corners := [4][2]int{
{0 - centerX, 0 - centerY},
{width - centerX, 0 - centerY},
{0 - centerX, height - centerY},
{width - centerX, height - centerY},
}
// 计算旋转后的四个角点位置
minX, minY, maxX, maxY := 0, 0, 0, 0
for i, p := range corners {
// 应用旋转
rotX := int(cos*float64(p[0]) - sin*float64(p[1]))
rotY := int(sin*float64(p[0]) + cos*float64(p[1]))
// 更新边界
if i == 0 || rotX < minX {
minX = rotX
}
if i == 0 || rotY < minY {
minY = rotY
}
if i == 0 || rotX > maxX {
maxX = rotX
}
if i == 0 || rotY > maxY {
maxY = rotY
}
}
// 创建新图像
newWidth, newHeight := maxX-minX+1, maxY-minY+1
newImage := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
newCenterX, newCenterY := newWidth/2, newHeight/2
// 预计算矩阵常量
invSin, invCos := math.Sin(-angle), math.Cos(-angle)
// 使用查找表存储sin/cos值避免重复计算
srcXOrigin := make([]int, newWidth)
srcYOrigin := make([]int, newHeight)
// 预计算X坐标的变换
for x := 0; x < newWidth; x++ {
srcXOrigin[x] = centerX
}
// 预计算Y坐标的变换
for y := 0; y < newHeight; y++ {
srcYOrigin[y] = centerY
}
// 批量处理像素
for y := 0; y < newHeight; y++ {
srcY := y - newCenterY
for x := 0; x < newWidth; x++ {
srcX := x - newCenterX
// 应用逆向旋转矩阵
origX := int(invCos*float64(srcX)-invSin*float64(srcY)) + centerX
origY := int(invSin*float64(srcX)+invCos*float64(srcY)) + centerY
// 检查是否在原图范围内
if origX >= 0 && origX < width && origY >= 0 && origY < height {
newImage.Set(x, y, s.Image.At(origX, origY))
}
}
}
s.Image = newImage
}
func (s *Sprite) Move(x, y int) {
s.Position.X += x
s.Position.Y += y
}
func (s *Sprite) Project(projectMatrix *ProjectMatrix) {
bounds := s.Image.Bounds()
// 计算四个角点投影后的位置
minX, minY, maxX, maxY := 0, 0, 0, 0
// 预先计算所有角点的投影,一次性确定边界
corners := [4]image.Point{
{bounds.Min.X, bounds.Min.Y},
{bounds.Max.X, bounds.Min.Y},
{bounds.Min.X, bounds.Max.Y},
{bounds.Max.X, bounds.Max.Y},
}
for i, corner := range corners {
projected := projectMatrix.ProjectPoint(corner)
if i == 0 {
minX, minY = projected.X, projected.Y
maxX, maxY = projected.X, projected.Y
} else {
if projected.X < minX {
minX = projected.X
}
if projected.Y < minY {
minY = projected.Y
}
if projected.X > maxX {
maxX = projected.X
}
if projected.Y > maxY {
maxY = projected.Y
}
}
}
// 创建新图像
newWidth, newHeight := maxX-minX+1, maxY-minY+1
newImage := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
// 计算逆矩阵
det := projectMatrix.Matrix[0][0]*projectMatrix.Matrix[1][1] -
projectMatrix.Matrix[0][1]*projectMatrix.Matrix[1][0]
if math.Abs(det) < 1e-6 {
// 矩阵接近不可逆
return
}
invDet := 1.0 / det
invMatrix := [2][2]float64{
{projectMatrix.Matrix[1][1] * invDet, -projectMatrix.Matrix[0][1] * invDet},
{-projectMatrix.Matrix[1][0] * invDet, projectMatrix.Matrix[0][0] * invDet},
}
// 预计算变换常量
m00, m01 := invMatrix[0][0], invMatrix[0][1]
m10, m11 := invMatrix[1][0], invMatrix[1][1]
// 使用直接内存访问优化像素设置
dstRGBA := newImage.Pix
srcImg, srcRGBA, _ := getImageBytes(s.Image)
// 批量处理像素
for y := 0; y < newHeight; y++ {
srcY := float64(y + minY)
for x := 0; x < newWidth; x++ {
srcX := float64(x + minX)
// 使用逆矩阵计算
origX := int(m00*srcX + m01*srcY)
origY := int(m10*srcX + m11*srcY)
// 检查边界
if origX >= bounds.Min.X && origX < bounds.Max.X &&
origY >= bounds.Min.Y && origY < bounds.Max.Y {
// 设置像素 - 直接使用indexing会更快
if srcRGBA != nil {
// 如果源图像是RGBA格式直接复制像素
dstIdx := (y*newImage.Stride + x*4)
srcIdx := ((origY-bounds.Min.Y)*srcImg.Stride + (origX-bounds.Min.X)*4)
dstRGBA[dstIdx] = srcRGBA[srcIdx] // R
dstRGBA[dstIdx+1] = srcRGBA[srcIdx+1] // G
dstRGBA[dstIdx+2] = srcRGBA[srcIdx+2] // B
dstRGBA[dstIdx+3] = srcRGBA[srcIdx+3] // A
} else {
// 否则使用通用方法
newImage.Set(x, y, s.Image.At(origX, origY))
}
}
}
}
s.Image = newImage
}
// getImageBytes 尝试提取图像的原始字节数据以加速访问
func getImageBytes(img image.Image) (*image.RGBA, []uint8, bool) {
if rgba, ok := img.(*image.RGBA); ok {
return rgba, rgba.Pix, true
}
return nil, nil, false
}