feat: 更新图像加载和精灵结构,支持延迟时间和多帧动画,添加GIF保存功能
This commit is contained in:
parent
2cf9da0896
commit
9c0b0d206d
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user