feat: 添加基础图形处理模块,包括精灵管理、绘制功能和图像渲染支持。
This commit is contained in:
parent
c03d1c20b8
commit
e3149f6079
28
draw/draw.go
Normal file
28
draw/draw.go
Normal 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
|
||||
}
|
41
model/2d.go
Normal file
41
model/2d.go
Normal 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
522
model/named_sprite_board.go
Normal 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
|
||||
}
|
387
model/named_sprite_board_test.go
Normal file
387
model/named_sprite_board_test.go
Normal 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
208
model/sprite.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user