From 9c0b0d206d1faf02d8c1737f001cb52c2673fa32 Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Tue, 13 May 2025 23:20:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=92=8C=E7=B2=BE=E7=81=B5=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=BB=B6=E8=BF=9F=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=92=8C=E5=A4=9A=E5=B8=A7=E5=8A=A8=E7=94=BB=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0GIF=E4=BF=9D=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sprite/image_loader.go | 21 +-- sprite/named_sprite_board.go | 250 +++++++++++++++++++++++++++++++++++ sprite/sprite.go | 60 ++++++++- 3 files changed, 316 insertions(+), 15 deletions(-) diff --git a/sprite/image_loader.go b/sprite/image_loader.go index 62f1705..ea36856 100644 --- a/sprite/image_loader.go +++ b/sprite/image_loader.go @@ -11,18 +11,18 @@ import ( ) // LoadImageFile 从文件加载图像,支持PNG、JPEG、GIF和静态WebP格式 -func LoadImageFile(filePath string) ([]image.Image, error) { +func LoadImageFile(filePath string) ([]image.Image, []int, error) { // 读取文件内容 fileData, err := os.ReadFile(filePath) if err != nil { - return nil, err + return nil, nil, err } return LoadImageData(fileData) } // LoadImageData 从字节数据加载图像,支持PNG、JPEG、GIF和静态WebP格式 -func LoadImageData(fileData []byte) ([]image.Image, error) { +func LoadImageData(fileData []byte) ([]image.Image, []int, error) { // 创建Reader reader := bytes.NewReader(fileData) @@ -34,11 +34,11 @@ func LoadImageData(fileData []byte) ([]image.Image, error) { // 尝试使用通用图像解码器(处理PNG、JPEG等) img, _, err := image.Decode(bytes.NewReader(fileData)) if err != nil { - return nil, err + return nil, nil, err } // 返回单帧图像 - return []image.Image{img}, nil + return []image.Image{img}, []int{0}, nil } // isGIF 检查数据是否为GIF格式 @@ -47,17 +47,17 @@ func isGIF(data []byte) bool { } // decodeGIF 解码GIF文件为图像帧序列 -func decodeGIF(reader io.Reader) ([]image.Image, error) { +func decodeGIF(reader io.Reader) ([]image.Image, []int, error) { // 解码GIF gifImg, err := gif.DecodeAll(reader) if err != nil { - return nil, err + return nil, nil, err } // 检查帧数量 frameCount := len(gifImg.Image) if frameCount == 0 { - return nil, nil + return nil, nil, nil } // 提取所有帧 @@ -66,13 +66,13 @@ func decodeGIF(reader io.Reader) ([]image.Image, error) { images[i] = frame } - return images, nil + return images, gifImg.Delay, nil } // LoadSpriteFromFile 从文件创建一个精灵 func LoadSpriteFromFile(name string, filePath string) (*Sprite, error) { // 加载图像 - images, err := LoadImageFile(filePath) + images, delays, err := LoadImageFile(filePath) if err != nil { return nil, err } @@ -81,6 +81,7 @@ func LoadSpriteFromFile(name string, filePath string) (*Sprite, error) { sprite := &Sprite{ Name: name, Images: images, + Delay: delays, CurrentFrame: 0, Position: image.Point{X: 0, Y: 0}, Index: 0, diff --git a/sprite/named_sprite_board.go b/sprite/named_sprite_board.go index 9e55177..b15c3f7 100644 --- a/sprite/named_sprite_board.go +++ b/sprite/named_sprite_board.go @@ -3,6 +3,7 @@ package sprite import ( "image" "image/draw" + "image/gif" "image/png" "log" "os" @@ -589,6 +590,197 @@ func (b *NamedSpriteBoard) RenderToImage() *image.RGBA { return img } +func (b *NamedSpriteBoard) RenderToAnimatedImage() (img []*image.RGBA, delays []int) { + // 查找所有精灵的最大总时间 + maxTotalTime := 0 + + // 遍历所有精灵查找最大总时间 + currentGroup := b.indexHead + for currentGroup != nil { + currentNode := currentGroup.Head + for currentNode != nil { + sprite := currentNode.Sprite + if sprite.FrameCount() > 1 && len(sprite.Delay) > 0 { + totalTime := sprite.GetTotalTime() + if totalTime > maxTotalTime { + maxTotalTime = totalTime + } + } + currentNode = currentNode.Next + } + currentGroup = currentGroup.Next + } + + // 如果没有动画精灵,返回单帧 + if maxTotalTime == 0 { + // 渲染一个静态帧 + staticFrame := b.RenderToImage() + return []*image.RGBA{staticFrame}, []int{10} // 默认延迟100ms + } + + // 获取画布边界 + minX, minY, maxX, maxY := b.GetRenderBounds() + width := maxX - minX + height := maxY - minY + + // 初始化结果数组 + img = make([]*image.RGBA, 0) + delays = make([]int, 0) + + // 跟踪每个精灵的当前帧索引 + spriteCurrentFrames := make(map[*Sprite]int) + + // 需要检查的时间点列表 - 初始包含所有精灵的帧变化时间点 + timePoints := make([]int, 0) + timePoints = append(timePoints, 0) // 起始点 + + // 收集所有精灵的帧变化时间点 + currentGroup = b.indexHead + for currentGroup != nil { + currentNode := currentGroup.Head + for currentNode != nil { + sprite := currentNode.Sprite + if sprite.FrameCount() > 1 && len(sprite.Delay) > 0 { + // 添加每个延迟增量点 + timePoints = append(timePoints, sprite.delayIncreased...) + } + currentNode = currentNode.Next + } + currentGroup = currentGroup.Next + } + + // 对时间点进行排序和去重 + sort.Ints(timePoints) + uniqueTimePoints := make([]int, 0) + lastTime := -1 + for _, t := range timePoints { + if t != lastTime { + uniqueTimePoints = append(uniqueTimePoints, t) + lastTime = t + } + } + + // 对每个时间点生成一帧 + lastFrameImage := (*image.RGBA)(nil) + + for i, timePoint := range uniqueTimePoints { + // 渲染当前时间点的帧 + currentFrame := image.NewRGBA(image.Rect(0, 0, width, height)) + frameChanged := false + + // 检查是否有任何精灵的帧发生变化 + currentGroup = b.indexHead + for currentGroup != nil { + currentNode := currentGroup.Head + for currentNode != nil { + sprite := currentNode.Sprite + + // 获取当前精灵在当前时间点的帧 + var currentImage image.Image + var frameIndex int + + if sprite.FrameCount() > 1 && len(sprite.Delay) > 0 { + // 使用延迟时间获取当前帧和帧索引 + currentImage, frameIndex = sprite.GetFrameOnDelay(timePoint) + + // 检查帧是否变化 + oldFrameIndex, exists := spriteCurrentFrames[sprite] + if !exists || oldFrameIndex != frameIndex { + frameChanged = true + spriteCurrentFrames[sprite] = frameIndex + } + } else { + // 静态精灵直接使用当前帧 + currentImage = sprite.GetCurrentImage() + } + + if currentImage != nil { + // 获取精灵的边界 + srcBounds := currentImage.Bounds() + + // 计算原始目标区域(相对于minX, minY偏移) + dstMinX := sprite.Position.X - minX + dstMinY := sprite.Position.Y - minY + dstMaxX := dstMinX + srcBounds.Dx() + dstMaxY := dstMinY + srcBounds.Dy() + + // 计算源图像的裁剪区域(如果精灵部分在画布外) + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + // 处理左边界超出 + if dstMinX < 0 { + srcMinX -= dstMinX // 向右移动源图像起点 + dstMinX = 0 + } + + // 处理上边界超出 + if dstMinY < 0 { + srcMinY -= dstMinY // 向下移动源图像起点 + dstMinY = 0 + } + + // 处理右边界超出 + if dstMaxX > width { + dstMaxX = width + } + + // 处理下边界超出 + if dstMaxY > height { + dstMaxY = height + } + + // 如果裁剪后的区域有效(宽度和高度都大于0) + if dstMinX < dstMaxX && dstMinY < dstMaxY { + dstRect := image.Rect(dstMinX, dstMinY, dstMaxX, dstMaxY) + srcPt := image.Point{X: srcMinX, Y: srcMinY} + + // 绘制裁剪后的精灵到画布上 + draw.Draw(currentFrame, dstRect, currentImage, srcPt, draw.Over) + } + } + + // 移动到下一个精灵 + currentNode = currentNode.Next + } + + // 移动到下一个索引组 + currentGroup = currentGroup.Next + } + + // 如果是第一帧或者帧内容有变化,则添加该帧 + if i == 0 || frameChanged { + // 计算这一帧的延迟时间 + delay := 0 + if i < len(uniqueTimePoints)-1 { + delay = uniqueTimePoints[i+1] - timePoint + } else { + delay = 10 // 最后一帧默认延迟100ms + } + + // 将当前帧添加到结果中 + img = append(img, currentFrame) + delays = append(delays, delay) + lastFrameImage = currentFrame + } else if lastFrameImage != nil { + // 如果帧没有变化,则累加到上一帧的延迟中 + if len(delays) > 0 { + if i < len(uniqueTimePoints)-1 { + delays[len(delays)-1] += uniqueTimePoints[i+1] - timePoint + } + } + } + } + + // 确保至少有一帧 + if len(img) == 0 { + staticFrame := b.RenderToImage() + return []*image.RGBA{staticFrame}, []int{10} + } + + return img, delays +} + func (b *NamedSpriteBoard) GetRenderBounds() (minX, minY, maxX, maxY int) { // 计算所有精灵图覆盖的最大区域 @@ -670,3 +862,61 @@ func (b *NamedSpriteBoard) SaveToPng(filename string) error { return nil } + +func (b *NamedSpriteBoard) SaveToGIF(filename string) error { + // 获取动画帧 + frames, delays := b.RenderToAnimatedImage() + if len(frames) == 0 { + // 如果没有动画帧,则使用单一图像 + img := b.RenderToImage() + + // 创建文件 + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + // 将RGBA图像转换为Paletted图像 + bounds := img.Bounds() + palettedImg := image.NewPaletted(bounds, nil) + draw.Draw(palettedImg, bounds, img, bounds.Min, draw.Src) + + // 将图像编码为GIF并写入文件 + if err := gif.EncodeAll(f, &gif.GIF{ + Image: []*image.Paletted{palettedImg}, + Delay: []int{0}, + }); err != nil { + return err + } + + return nil + } + + // 多帧动画情况 + palettedFrames := make([]*image.Paletted, len(frames)) + + // 转换所有帧为Paletted图像 + for i, frame := range frames { + bounds := frame.Bounds() + palettedFrames[i] = image.NewPaletted(bounds, nil) + draw.Draw(palettedFrames[i], bounds, frame, bounds.Min, draw.Src) + } + + // 创建文件 + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + // 将图像编码为GIF并写入文件 + if err := gif.EncodeAll(f, &gif.GIF{ + Image: palettedFrames, + Delay: delays, + }); err != nil { + return err + } + + return nil +} diff --git a/sprite/sprite.go b/sprite/sprite.go index e950291..59bcb2c 100644 --- a/sprite/sprite.go +++ b/sprite/sprite.go @@ -6,11 +6,13 @@ import ( ) type Sprite struct { - Name string - Position image.Point - Images []image.Image - CurrentFrame int - Index int + Name string + Position image.Point + Images []image.Image + Delay []int + delayIncreased []int + CurrentFrame int + Index int } func (s *Sprite) SetImage(img *image.RGBA) { @@ -260,6 +262,54 @@ func (s *Sprite) GetFrame(index int) image.Image { return s.Images[index] } +func (s *Sprite) GetFrameOnDelay(delay int) (image.Image, int) { + if s.delayIncreased == nil || len(s.delayIncreased) != len(s.Delay) { + s.delayIncreased = make([]int, len(s.Delay)) + for i := range s.Delay { + if i == 0 { + s.delayIncreased[i] = s.Delay[i] + } else { + s.delayIncreased[i] = s.Delay[i] + s.delayIncreased[i-1] + } + } + } + + if delay < 0 { + return s.Images[0], 0 + } else if delay >= s.delayIncreased[len(s.delayIncreased)-1] { + return s.Images[len(s.Images)-1], len(s.Images) - 1 + } + + // 使用二分查找代替线性查找 + left, right := 0, len(s.delayIncreased)-1 + for left <= right { + mid := left + (right-left)/2 + if delay < s.delayIncreased[mid] { + if mid == 0 || delay >= s.delayIncreased[mid-1] { + return s.Images[mid], mid + } + right = mid - 1 + } else { + left = mid + 1 + } + } + return s.Images[0], 0 +} + +func (s *Sprite) GetTotalTime() int { + if s.delayIncreased == nil || len(s.delayIncreased) != len(s.Delay) { + s.delayIncreased = make([]int, len(s.Delay)) + for i := range s.Delay { + if i == 0 { + s.delayIncreased[i] = s.Delay[i] + } else { + s.delayIncreased[i] = s.Delay[i] + s.delayIncreased[i-1] + } + } + } + return s.delayIncreased[len(s.delayIncreased)-1] +} + // FrameCount 返回图像帧数量 func (s *Sprite) FrameCount() int { return len(s.Images)