From 5212270c4404025703080327b6b77fa165516d54 Mon Sep 17 00:00:00 2001 From: Ziemas Date: Tue, 7 Jul 2020 02:13:06 +0200 Subject: [PATCH] Split up DCC/DC6 animation implementations, decode directions on demand (#548) * Split DCC and decode direction on demand * Clean up * More cleanup * Make dc6 animation it's own type * Make base animation private * Also decode DC6 directions on demand --- d2common/d2fileformats/d2dcc/dcc.go | 24 +-- d2core/d2asset/animation.go | 231 ++++------------------------ d2core/d2asset/animation_manager.go | 16 +- d2core/d2asset/composite.go | 3 + d2core/d2asset/dc6_animation.go | 154 +++++++++++++++++++ d2core/d2asset/dcc_animation.go | 141 +++++++++++++++++ 6 files changed, 347 insertions(+), 222 deletions(-) create mode 100644 d2core/d2asset/dc6_animation.go create mode 100644 d2core/d2asset/dcc_animation.go diff --git a/d2common/d2fileformats/d2dcc/dcc.go b/d2common/d2fileformats/d2dcc/dcc.go index e136b080..918ba436 100644 --- a/d2common/d2fileformats/d2dcc/dcc.go +++ b/d2common/d2fileformats/d2dcc/dcc.go @@ -15,12 +15,15 @@ type DCC struct { Version int NumberOfDirections int FramesPerDirection int - Directions []*DCCDirection + directionOffsets []int + fileData []byte } // Load loads a DCC file. func Load(fileData []byte) (*DCC, error) { - result := &DCC{} + result := &DCC{ + fileData: fileData, + } var bm = d2common.CreateBitMuncher(fileData, 0) @@ -40,18 +43,17 @@ func Load(fileData []byte) (*DCC, error) { bm.GetInt32() // TotalSizeCoded - directionOffsets := make([]int, result.NumberOfDirections) + result.directionOffsets = make([]int, result.NumberOfDirections) for i := 0; i < result.NumberOfDirections; i++ { - directionOffsets[i] = int(bm.GetInt32()) - } - - result.Directions = make([]*DCCDirection, result.NumberOfDirections) - - for i := 0; i < result.NumberOfDirections; i++ { - result.Directions[i] = CreateDCCDirection(d2common.CreateBitMuncher( - fileData, directionOffsets[i]*directionOffsetMultiplier), result) + result.directionOffsets[i] = int(bm.GetInt32()) } return result, nil } + +// DecodeDirection decodes and returns the given direction +func (dcc *DCC) DecodeDirection(direction int) *DCCDirection { + return CreateDCCDirection(d2common.CreateBitMuncher(dcc.fileData, + dcc.directionOffsets[direction]*directionOffsetMultiplier), dcc) +} diff --git a/d2core/d2asset/animation.go b/d2core/d2asset/animation.go index b2d94ff8..4ebc047c 100644 --- a/d2core/d2asset/animation.go +++ b/d2core/d2asset/animation.go @@ -4,14 +4,12 @@ import ( "errors" "image" "image/color" - "math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc" ) @@ -35,12 +33,13 @@ type animationFrame struct { } type animationDirection struct { - frames []*animationFrame + decoded bool + frames []*animationFrame } -// Animation has directionality, play modes, and frame counting -type Animation struct { - directions []*animationDirection +// animation has directionality, play modes, and frame counting +type animation struct { + directions []animationDirection colorMod color.Color frameIndex int directionIndex int @@ -56,179 +55,15 @@ type Animation struct { hasSubLoop bool // runs after first animation ends } -// CreateAnimationFromDCC creates an animation from d2dcc.DCC and d2dat.DATPalette -func CreateAnimationFromDCC(renderer d2iface.Renderer, dcc *d2dcc.DCC, palette d2iface.Palette, - transparency int) (d2iface.Animation, error) { - animation := &Animation{ - playLength: defaultPlayLength, - playLoop: true, - } - - for directionIndex, dccDirection := range dcc.Directions { - for _, dccFrame := range dccDirection.Frames { - minX, minY := math.MaxInt32, math.MaxInt32 - maxX, maxY := math.MinInt32, math.MinInt32 - - for _, dccFrame := range dccDirection.Frames { - minX = d2common.MinInt(minX, dccFrame.Box.Left) - minY = d2common.MinInt(minY, dccFrame.Box.Top) - maxX = d2common.MaxInt(maxX, dccFrame.Box.Right()) - maxY = d2common.MaxInt(maxY, dccFrame.Box.Bottom()) - } - - frameWidth := maxX - minX - frameHeight := maxY - minY - - const bytesPerPixel = 4 - pixels := make([]byte, frameWidth*frameHeight*bytesPerPixel) - - for y := 0; y < frameHeight; y++ { - for x := 0; x < frameWidth; x++ { - paletteIndex := dccFrame.PixelData[y*frameWidth+x] - - if paletteIndex == 0 { - continue - } - - palColor := palette.GetColors()[paletteIndex] - offset := (x + y*frameWidth) * bytesPerPixel - pixels[offset] = palColor.R() - pixels[offset+1] = palColor.G() - pixels[offset+2] = palColor.B() - pixels[offset+3] = byte(transparency) - } - } - - sfc, err := renderer.NewSurface(frameWidth, frameHeight, d2iface.FilterNearest) - if err != nil { - return nil, err - } - - if err := sfc.ReplacePixels(pixels); err != nil { - return nil, err - } - - if directionIndex >= len(animation.directions) { - animation.directions = append(animation.directions, new(animationDirection)) - } - - direction := animation.directions[directionIndex] - direction.frames = append(direction.frames, &animationFrame{ - width: dccFrame.Width, - height: dccFrame.Height, - offsetX: minX, - offsetY: minY, - image: sfc, - }) - } - } - - return animation, nil -} - -// CreateAnimationFromDC6 creates an Animation from d2dc6.DC6 and d2dat.DATPalette -func CreateAnimationFromDC6(renderer d2iface.Renderer, dc6 *d2dc6.DC6, - palette d2iface.Palette) (d2iface.Animation, error) { - animation := &Animation{ - playLength: defaultPlayLength, - playLoop: true, - originAtBottom: true, - } - - for frameIndex, dc6Frame := range dc6.Frames { - sfc, err := renderer.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), - d2iface.FilterNearest) - if err != nil { - return nil, err - } - - indexData := make([]int, dc6Frame.Width*dc6Frame.Height) - for i := range indexData { - indexData[i] = -1 - } - - x := 0 - y := int(dc6Frame.Height) - 1 - offset := 0 - - for { - b := int(dc6Frame.FrameData[offset]) - offset++ - - if b == 0x80 { - if y == 0 { - break - } - y-- - x = 0 - } else if b&0x80 > 0 { - transparentPixels := b & 0x7f - for i := 0; i < transparentPixels; i++ { - indexData[x+y*int(dc6Frame.Width)+i] = -1 - } - x += transparentPixels - } else { - for i := 0; i < b; i++ { - indexData[x+y*int(dc6Frame.Width)+i] = int(dc6Frame.FrameData[offset]) - offset++ - } - x += b - } - } - - bytesPerPixel := 4 - colorData := make([]byte, int(dc6Frame.Width)*int(dc6Frame.Height)*bytesPerPixel) - - for i := 0; i < int(dc6Frame.Width*dc6Frame.Height); i++ { - // TODO: Is this == -1 or < 1? - if indexData[i] < 1 { - continue - } - - c := palette.GetColors()[indexData[i]] - colorData[i*bytesPerPixel] = c.R() - colorData[i*bytesPerPixel+1] = c.G() - colorData[i*bytesPerPixel+2] = c.B() - colorData[i*bytesPerPixel+3] = c.A() - } - - if err := sfc.ReplacePixels(colorData); err != nil { - return nil, err - } - - directionIndex := frameIndex / int(dc6.FramesPerDirection) - if directionIndex >= len(animation.directions) { - animation.directions = append(animation.directions, new(animationDirection)) - } - - direction := animation.directions[directionIndex] - direction.frames = append(direction.frames, &animationFrame{ - width: int(dc6Frame.Width), - height: int(dc6Frame.Height), - offsetX: int(dc6Frame.OffsetX), - offsetY: int(dc6Frame.OffsetY), - image: sfc, - }) - } - - return animation, nil -} - -// Clone creates a copy of the animation -func (a *Animation) Clone() d2iface.Animation { - animation := *a - return &animation -} - // SetSubLoop sets a sub loop for the animation -func (a *Animation) SetSubLoop(startFrame, endFrame int) { +func (a *animation) SetSubLoop(startFrame, endFrame int) { a.subStartingFrame = startFrame a.subEndingFrame = endFrame a.hasSubLoop = true } // Advance advances the animation state -func (a *Animation) Advance(elapsed float64) error { +func (a *animation) Advance(elapsed float64) error { if a.playMode == playModePause { return nil } @@ -278,7 +113,7 @@ func (a *Animation) Advance(elapsed float64) error { } // Render renders the animation to the given surface -func (a *Animation) Render(target d2iface.Surface) error { +func (a *animation) Render(target d2iface.Surface) error { direction := a.directions[a.directionIndex] frame := direction.frames[a.frameIndex] @@ -295,7 +130,7 @@ func (a *Animation) Render(target d2iface.Surface) error { } // RenderFromOrigin renders the animation from the animation origin -func (a *Animation) RenderFromOrigin(target d2iface.Surface) error { +func (a *animation) RenderFromOrigin(target d2iface.Surface) error { if a.originAtBottom { direction := a.directions[a.directionIndex] frame := direction.frames[a.frameIndex] @@ -308,7 +143,7 @@ func (a *Animation) RenderFromOrigin(target d2iface.Surface) error { } // RenderSection renders the section of the animation frame enclosed by bounds -func (a *Animation) RenderSection(sfc d2iface.Surface, bound image.Rectangle) error { +func (a *animation) RenderSection(sfc d2iface.Surface, bound image.Rectangle) error { direction := a.directions[a.directionIndex] frame := direction.frames[a.frameIndex] @@ -322,7 +157,7 @@ func (a *Animation) RenderSection(sfc d2iface.Surface, bound image.Rectangle) er } // GetFrameSize gets the Size(width, height) of a indexed frame. -func (a *Animation) GetFrameSize(frameIndex int) (width, height int, err error) { +func (a *animation) GetFrameSize(frameIndex int) (width, height int, err error) { direction := a.directions[a.directionIndex] if frameIndex >= len(direction.frames) { return 0, 0, errors.New("invalid frame index") @@ -334,13 +169,13 @@ func (a *Animation) GetFrameSize(frameIndex int) (width, height int, err error) } // GetCurrentFrameSize gets the Size(width, height) of the current frame. -func (a *Animation) GetCurrentFrameSize() (width, height int) { +func (a *animation) GetCurrentFrameSize() (width, height int) { width, height, _ = a.GetFrameSize(a.frameIndex) return width, height } // GetFrameBounds gets maximum Size(width, height) of all frame. -func (a *Animation) GetFrameBounds() (maxWidth, maxHeight int) { +func (a *animation) GetFrameBounds() (maxWidth, maxHeight int) { maxWidth, maxHeight = 0, 0 direction := a.directions[a.directionIndex] @@ -353,33 +188,33 @@ func (a *Animation) GetFrameBounds() (maxWidth, maxHeight int) { } // GetCurrentFrame gets index of current frame in animation -func (a *Animation) GetCurrentFrame() int { +func (a *animation) GetCurrentFrame() int { return a.frameIndex } // GetFrameCount gets number of frames in animation -func (a *Animation) GetFrameCount() int { +func (a *animation) GetFrameCount() int { direction := a.directions[a.directionIndex] return len(direction.frames) } // IsOnFirstFrame gets if the animation on its first frame -func (a *Animation) IsOnFirstFrame() bool { +func (a *animation) IsOnFirstFrame() bool { return a.frameIndex == 0 } // IsOnLastFrame gets if the animation on its last frame -func (a *Animation) IsOnLastFrame() bool { +func (a *animation) IsOnLastFrame() bool { return a.frameIndex == a.GetFrameCount()-1 } // GetDirectionCount gets the number of animation direction -func (a *Animation) GetDirectionCount() int { +func (a *animation) GetDirectionCount() int { return len(a.directions) } // SetDirection places the animation in the direction of an animation -func (a *Animation) SetDirection(directionIndex int) error { +func (a *animation) SetDirection(directionIndex int) error { const smallestInvalidDirectionIndex = 64 if directionIndex >= smallestInvalidDirectionIndex { return errors.New("invalid direction index") @@ -392,12 +227,12 @@ func (a *Animation) SetDirection(directionIndex int) error { } // GetDirection get the current animation direction -func (a *Animation) GetDirection() int { +func (a *animation) GetDirection() int { return a.directionIndex } // SetCurrentFrame sets animation at a specific frame -func (a *Animation) SetCurrentFrame(frameIndex int) error { +func (a *animation) SetCurrentFrame(frameIndex int) error { if frameIndex >= a.GetFrameCount() { return errors.New("invalid frame index") } @@ -409,69 +244,69 @@ func (a *Animation) SetCurrentFrame(frameIndex int) error { } // Rewind animation to beginning -func (a *Animation) Rewind() { +func (a *animation) Rewind() { _ = a.SetCurrentFrame(0) } // PlayForward plays animation forward -func (a *Animation) PlayForward() { +func (a *animation) PlayForward() { a.playMode = playModeForward a.lastFrameTime = 0 } // PlayBackward plays animation backward -func (a *Animation) PlayBackward() { +func (a *animation) PlayBackward() { a.playMode = playModeBackward a.lastFrameTime = 0 } // Pause animation -func (a *Animation) Pause() { +func (a *animation) Pause() { a.playMode = playModePause a.lastFrameTime = 0 } // SetPlayLoop sets whether to loop the animation -func (a *Animation) SetPlayLoop(loop bool) { +func (a *animation) SetPlayLoop(loop bool) { a.playLoop = loop } // SetPlaySpeed sets play speed of the animation -func (a *Animation) SetPlaySpeed(playSpeed float64) { +func (a *animation) SetPlaySpeed(playSpeed float64) { a.SetPlayLength(playSpeed * float64(a.GetFrameCount())) } // SetPlayLength sets the Animation's play length in seconds -func (a *Animation) SetPlayLength(playLength float64) { +func (a *animation) SetPlayLength(playLength float64) { // TODO refactor to use time.Duration instead of float64 a.playLength = playLength a.lastFrameTime = 0 } // SetPlayLengthMs sets the Animation's play length in milliseconds -func (a *Animation) SetPlayLengthMs(playLengthMs int) { +func (a *animation) SetPlayLengthMs(playLengthMs int) { // TODO remove this method const millisecondsPerSecond = 1000.0 a.SetPlayLength(float64(playLengthMs) / millisecondsPerSecond) } // SetColorMod sets the Animation's color mod -func (a *Animation) SetColorMod(colorMod color.Color) { +func (a *animation) SetColorMod(colorMod color.Color) { a.colorMod = colorMod } // GetPlayedCount gets the number of times the application played -func (a *Animation) GetPlayedCount() int { +func (a *animation) GetPlayedCount() int { return a.playedCount } // ResetPlayedCount resets the play count -func (a *Animation) ResetPlayedCount() { +func (a *animation) ResetPlayedCount() { a.playedCount = 0 } // SetBlend sets the Animation alpha blending status -func (a *Animation) SetBlend(blend bool) { +func (a *animation) SetBlend(blend bool) { if blend { a.compositeMode = d2enum.CompositeModeLighter } else { diff --git a/d2core/d2asset/animation_manager.go b/d2core/d2asset/animation_manager.go index 9226205c..d8732dc3 100644 --- a/d2core/d2asset/animation_manager.go +++ b/d2core/d2asset/animation_manager.go @@ -39,7 +39,7 @@ func (am *animationManager) LoadAnimation( transparency int ) (d2interface.Animation, error) { cachePath := fmt.Sprintf("%s;%s;%d", animationPath, palettePath, transparency) if animation, found := am.cache.Retrieve(cachePath); found { - return animation.(*Animation).Clone(), nil + return animation.(d2interface.Animation).Clone(), nil } var animation d2interface.Animation @@ -47,32 +47,22 @@ func (am *animationManager) LoadAnimation( ext := strings.ToLower(filepath.Ext(animationPath)) switch ext { case ".dc6": - dc6, err := loadDC6(animationPath) - if err != nil { - return nil, err - } - palette, err := LoadPalette(palettePath) if err != nil { return nil, err } - animation, err = CreateAnimationFromDC6(am.renderer, dc6, palette) + animation, err = CreateDC6Animation(am.renderer, animationPath, palette) if err != nil { return nil, err } case ".dcc": - dcc, err := loadDCC(animationPath) - if err != nil { - return nil, err - } - palette, err := LoadPalette(palettePath) if err != nil { return nil, err } - animation, err = CreateAnimationFromDCC(am.renderer, dcc, palette, transparency) + animation, err = CreateDCCAnimation(am.renderer, animationPath, palette, transparency) if err != nil { return nil, err } diff --git a/d2core/d2asset/composite.go b/d2core/d2asset/composite.go index 6fb70387..0d5df18f 100644 --- a/d2core/d2asset/composite.go +++ b/d2core/d2asset/composite.go @@ -3,6 +3,7 @@ package d2asset import ( "errors" "fmt" + "log" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data" @@ -249,6 +250,8 @@ func (c *Composite) createMode(animationMode, weaponClass string) (*compositeMod transparency = 192 case d2enum.DrawEffectModulate: blend = true + default: + log.Println("Unhandled DrawEffect ", cofLayer.DrawEffect, " requested for layer ", cofLayer.Type.String(), " of ", c.token) } } diff --git a/d2core/d2asset/dc6_animation.go b/d2core/d2asset/dc6_animation.go new file mode 100644 index 00000000..98d10a02 --- /dev/null +++ b/d2core/d2asset/dc6_animation.go @@ -0,0 +1,154 @@ +package d2asset + +import ( + "errors" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc" + d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" +) + +// DC6Animation is an animation made from a DC6 file +type DC6Animation struct { + animation + dc6Path string + palette d2iface.Palette + renderer d2iface.Renderer +} + +// CreateDC6Animation creates an Animation from d2dc6.DC6 and d2dat.DATPalette +func CreateDC6Animation(renderer d2iface.Renderer, dc6Path string, + palette d2iface.Palette) (d2iface.Animation, error) { + dc6, err := loadDC6(dc6Path) + if err != nil { + return nil, err + } + + animation := animation{ + directions: make([]animationDirection, dc6.Directions), + playLength: defaultPlayLength, + playLoop: true, + originAtBottom: true, + } + + anim := DC6Animation{ + animation: animation, + dc6Path: dc6Path, + palette: palette, + renderer: renderer, + } + + err = anim.SetDirection(0) + + return &anim, err +} + +// SetDirection decodes and sets the direction +func (a *DC6Animation) SetDirection(directionIndex int) error { + const smallestInvalidDirectionIndex = 64 + if directionIndex >= smallestInvalidDirectionIndex { + return errors.New("invalid direction index") + } + + direction := d2dcc.Dir64ToDcc(directionIndex, len(a.directions)) + if !a.directions[direction].decoded { + err := a.decodeDirection(direction) + if err != nil { + return err + } + } + + a.directionIndex = direction + a.frameIndex = 0 + + return nil +} + +func (a *DC6Animation) decodeDirection(directionIndex int) error { + dc6, err := loadDC6(a.dc6Path) + if err != nil { + return err + } + + startFrame := directionIndex * int(dc6.FramesPerDirection) + + for i := 0; i < int(dc6.FramesPerDirection); i++ { + dc6Frame := dc6.Frames[startFrame+i] + + sfc, err := a.renderer.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), + d2iface.FilterNearest) + if err != nil { + return err + } + + indexData := make([]int, dc6Frame.Width*dc6Frame.Height) + for i := range indexData { + indexData[i] = -1 + } + + x := 0 + y := int(dc6Frame.Height) - 1 + offset := 0 + + for { + b := int(dc6Frame.FrameData[offset]) + offset++ + + if b == 0x80 { + if y == 0 { + break + } + y-- + x = 0 + } else if b&0x80 > 0 { + transparentPixels := b & 0x7f + for i := 0; i < transparentPixels; i++ { + indexData[x+y*int(dc6Frame.Width)+i] = -1 + } + x += transparentPixels + } else { + for i := 0; i < b; i++ { + indexData[x+y*int(dc6Frame.Width)+i] = int(dc6Frame.FrameData[offset]) + offset++ + } + x += b + } + } + + bytesPerPixel := 4 + colorData := make([]byte, int(dc6Frame.Width)*int(dc6Frame.Height)*bytesPerPixel) + + for i := 0; i < int(dc6Frame.Width*dc6Frame.Height); i++ { + // TODO: Is this == -1 or < 1? + if indexData[i] < 1 { + continue + } + + c := a.palette.GetColors()[indexData[i]] + colorData[i*bytesPerPixel] = c.R() + colorData[i*bytesPerPixel+1] = c.G() + colorData[i*bytesPerPixel+2] = c.B() + colorData[i*bytesPerPixel+3] = c.A() + } + + if err := sfc.ReplacePixels(colorData); err != nil { + return err + } + + a.directions[directionIndex].decoded = true + a.directions[directionIndex].frames = append(a.directions[directionIndex].frames, &animationFrame{ + width: int(dc6Frame.Width), + height: int(dc6Frame.Height), + offsetX: int(dc6Frame.OffsetX), + offsetY: int(dc6Frame.OffsetY), + image: sfc, + }) + } + + return nil +} + +// Clone creates a copy of the animation +func (a *DC6Animation) Clone() d2iface.Animation { + animation := *a + return &animation +} diff --git a/d2core/d2asset/dcc_animation.go b/d2core/d2asset/dcc_animation.go new file mode 100644 index 00000000..f40fae64 --- /dev/null +++ b/d2core/d2asset/dcc_animation.go @@ -0,0 +1,141 @@ +package d2asset + +import ( + "errors" + "math" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc" + d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" +) + +// DCCAnimation represens an animation decoded from DCC +type DCCAnimation struct { + animation + dccPath string + transparency int + palette d2iface.Palette + renderer d2iface.Renderer +} + +// CreateAnimationFromDCC creates an animation from d2dcc.DCC and d2dat.DATPalette +func CreateDCCAnimation(renderer d2iface.Renderer, dccPath string, palette d2iface.Palette, + transparency int) (d2iface.Animation, error) { + + dcc, err := loadDCC(dccPath) + if err != nil { + return nil, err + } + + anim := animation{ + playLength: defaultPlayLength, + playLoop: true, + directions: make([]animationDirection, dcc.NumberOfDirections), + } + + DCC := DCCAnimation{ + animation: anim, + dccPath: dccPath, + palette: palette, + renderer: renderer, + transparency: transparency, + } + + err = DCC.SetDirection(0) + if err != nil { + return nil, err + } + + return &DCC, nil +} + +// Clone creates a copy of the animation +func (a *DCCAnimation) Clone() d2iface.Animation { + animation := *a + return &animation +} + +// SetDirection places the animation in the direction of an animation +func (a *DCCAnimation) SetDirection(directionIndex int) error { + const smallestInvalidDirectionIndex = 64 + if directionIndex >= smallestInvalidDirectionIndex { + return errors.New("invalid direction index") + } + + direction := d2dcc.Dir64ToDcc(directionIndex, len(a.directions)) + if !a.directions[direction].decoded { + err := a.decodeDirection(direction) + if err != nil { + return err + } + } + + a.directionIndex = direction + a.frameIndex = 0 + + return nil +} + +func (a *DCCAnimation) decodeDirection(directionIndex int) error { + dcc, err := loadDCC(a.dccPath) + if err != nil { + return err + } + + direction := dcc.DecodeDirection(directionIndex) + + minX, minY := math.MaxInt32, math.MaxInt32 + maxX, maxY := math.MinInt32, math.MinInt32 + + for _, dccFrame := range direction.Frames { + minX = d2common.MinInt(minX, dccFrame.Box.Left) + minY = d2common.MinInt(minY, dccFrame.Box.Top) + maxX = d2common.MaxInt(maxX, dccFrame.Box.Right()) + maxY = d2common.MaxInt(maxY, dccFrame.Box.Bottom()) + } + + for _, dccFrame := range direction.Frames { + frameWidth := maxX - minX + frameHeight := maxY - minY + + const bytesPerPixel = 4 + pixels := make([]byte, frameWidth*frameHeight*bytesPerPixel) + + for y := 0; y < frameHeight; y++ { + for x := 0; x < frameWidth; x++ { + paletteIndex := dccFrame.PixelData[y*frameWidth+x] + + if paletteIndex == 0 { + continue + } + + palColor := a.palette.GetColors()[paletteIndex] + offset := (x + y*frameWidth) * bytesPerPixel + pixels[offset] = palColor.R() + pixels[offset+1] = palColor.G() + pixels[offset+2] = palColor.B() + pixels[offset+3] = byte(a.transparency) + } + } + + sfc, err := a.renderer.NewSurface(frameWidth, frameHeight, d2iface.FilterNearest) + if err != nil { + return err + } + + if err := sfc.ReplacePixels(pixels); err != nil { + return err + } + + a.directions[directionIndex].decoded = true + a.directions[directionIndex].frames = append(a.directions[directionIndex].frames, &animationFrame{ + width: dccFrame.Width, + height: dccFrame.Height, + offsetX: minX, + offsetY: minY, + image: sfc, + }) + } + + return nil +}