feat: 更新图像加载和精灵结构,支持延迟时间和多帧动画,添加GIF保存功能

This commit is contained in:
lixiangwuxian 2025-05-13 23:20:17 +08:00
parent 2cf9da0896
commit 9c0b0d206d
3 changed files with 316 additions and 15 deletions

View File

@ -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,

View File

@ -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
}

View File

@ -9,6 +9,8 @@ type Sprite struct {
Name string
Position image.Point
Images []image.Image
Delay []int
delayIncreased []int
CurrentFrame int
Index int
}
@ -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)