diff --git a/draw/draw.go b/draw/draw.go new file mode 100644 index 0000000..3ca5936 --- /dev/null +++ b/draw/draw.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..259920e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.lxtend.com/lixiangwuxian/imagedd + +go 1.24.2 diff --git a/model/2d.go b/model/2d.go new file mode 100644 index 0000000..f072f7d --- /dev/null +++ b/model/2d.go @@ -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), + } +} diff --git a/model/named_sprite_board.go b/model/named_sprite_board.go new file mode 100644 index 0000000..e5aba26 --- /dev/null +++ b/model/named_sprite_board.go @@ -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 +} diff --git a/model/named_sprite_board_test.go b/model/named_sprite_board_test.go new file mode 100644 index 0000000..051f2ec --- /dev/null +++ b/model/named_sprite_board_test.go @@ -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) + } +} diff --git a/model/sprite.go b/model/sprite.go new file mode 100644 index 0000000..dac8748 --- /dev/null +++ b/model/sprite.go @@ -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 +}