Skip to content

Commit

Permalink
Screenshot and GIF recording capability (OpenDiablo2#310)
Browse files Browse the repository at this point in the history
* Configuration cleanup

* Cleanup

* Gif animation and screenshot support
  • Loading branch information
FooSoft authored Feb 23, 2020
1 parent e4c84c4 commit 423cef3
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 2 deletions.
15 changes: 15 additions & 0 deletions d2core/d2render/ebiten/ebiten_surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ebiten

import (
"fmt"
"image"
"image/color"

"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
Expand Down Expand Up @@ -106,3 +107,17 @@ func (s *ebitenSurface) GetDepth() int {
func (s *ebitenSurface) ReplacePixels(pixels []byte) error {
return s.image.ReplacePixels(pixels)
}

func (s *ebitenSurface) Screenshot() *image.RGBA {
width, height := s.GetSize()
bounds := image.Rectangle{image.Point{0, 0}, image.Point{width, height}}
image := image.NewRGBA(bounds)

for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
image.Set(x, y, s.image.At(x, y))
}
}

return image
}
2 changes: 2 additions & 0 deletions d2core/d2render/surface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package d2render

import (
"image"
"image/color"
)

Expand All @@ -19,4 +20,5 @@ type Surface interface {
PushTranslation(x, y int)
Render(surface Surface) error
ReplacePixels(pixels []byte) error
Screenshot() *image.RGBA
}
120 changes: 118 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package main

import (
"bytes"
"errors"
"fmt"
"image"
"image/gif"
"image/png"
"log"
"os"
"runtime"
"strconv"
"sync"

"gopkg.in/alecthomas/kingpin.v2"

Expand Down Expand Up @@ -36,10 +41,22 @@ var GitBranch string
// GitCommit is set by the CI build process to the commit hash
var GitCommit string

type captureState int

const (
captureStateNone captureState = iota
captureStateFrame
captureStateGif
)

var singleton struct {
lastTime float64
showFPS bool
timeScale float64

captureState captureState
capturePath string
captureFrames []*image.RGBA
}

func main() {
Expand Down Expand Up @@ -103,6 +120,19 @@ func initialize() error {
d2render.SetFullScreen(fullscreen)
d2term.OutputInfo("fullscreen is now: %v", fullscreen)
})
d2term.BindAction("capframe", "captures a still frame", func(path string) {
singleton.captureState = captureStateFrame
singleton.capturePath = path
singleton.captureFrames = nil
})
d2term.BindAction("capgifstart", "captures an animation (start)", func(path string) {
singleton.captureState = captureStateGif
singleton.capturePath = path
singleton.captureFrames = nil
})
d2term.BindAction("capgifstop", "captures an animation (stop)", func() {
singleton.captureState = captureStateNone
})
d2term.BindAction("vsync", "toggles vsync", func() {
vsync := !d2render.GetVSyncEnabled()
d2render.SetVSyncEnabled(vsync)
Expand Down Expand Up @@ -212,14 +242,100 @@ func render(target d2render.Surface) error {
return err
}

if err := d2term.Render(target); err != nil {
if err := renderDebug(target); err != nil {
return err
}

if err := renderDebug(target); err != nil {
if err := renderCapture(target); err != nil {
return err
}

if err := d2term.Render(target); err != nil {
return err
}

return nil
}

func renderCapture(target d2render.Surface) error {
cleanupCapture := func() {
singleton.captureState = captureStateNone
singleton.capturePath = ""
singleton.captureFrames = nil
}

switch singleton.captureState {
case captureStateFrame:
defer cleanupCapture()

fp, err := os.Create(singleton.capturePath)
if err != nil {
return err
}

defer fp.Close()

screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil {
return err
}

log.Printf("saved frame to %s", singleton.capturePath)
break
case captureStateGif:
screenshot := target.Screenshot()
singleton.captureFrames = append(singleton.captureFrames, screenshot)
break
case captureStateNone:
if len(singleton.captureFrames) > 0 {
defer cleanupCapture()

fp, err := os.Create(singleton.capturePath)
if err != nil {
return err
}

defer fp.Close()

var (
framesTotal = len(singleton.captureFrames)
framesPal = make([]*image.Paletted, framesTotal)
frameDelays = make([]int, framesTotal)
framesPerCpu = framesTotal / runtime.NumCPU()
)

var waitGroup sync.WaitGroup
for i := 0; i < framesTotal; i += framesPerCpu {
waitGroup.Add(1)
go func(start, end int) {
defer waitGroup.Done()

for j := start; j < end; j++ {
var buffer bytes.Buffer
if err := gif.Encode(&buffer, singleton.captureFrames[j], nil); err != nil {
panic(err)
}

framePal, err := gif.Decode(&buffer)
if err != nil {
panic(err)
}

framesPal[j] = framePal.(*image.Paletted)
frameDelays[j] = 5
}
}(i, d2common.MinInt(i+framesPerCpu, framesTotal))
}

waitGroup.Wait()

if err := gif.EncodeAll(fp, &gif.GIF{Image: framesPal, Delay: frameDelays}); err != nil {
return err
}

log.Printf("saved animation to %s", singleton.capturePath)
}
}
return nil
}

Expand Down

0 comments on commit 423cef3

Please sign in to comment.