diff --git a/.gitignore b/.gitignore index cf56a2a..a8494fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ a2sdl.exe frontend/a2sdl/a2sdl frontend/a2sdl/*.woz frontend/a2sdl/*.dsk -frontend/a2fyne/a2fyne \ No newline at end of file +frontend/a2fyne/a2fyne +frontend/headless/headless +frontend/*/snapshot.gif +frontend/*/snapshot.png \ No newline at end of file diff --git a/frontend/headless/main.go b/frontend/headless/main.go index 7055215..cc88466 100644 --- a/frontend/headless/main.go +++ b/frontend/headless/main.go @@ -3,8 +3,10 @@ package main import ( "bufio" "fmt" + "image/gif" "os" "strings" + "time" "github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/screen" @@ -88,6 +90,9 @@ func main() { case "return": fe.keyChannel <- 13 + case "gif": + SaveGif(a, "snapshot.gif") + case "help": fmt.Print(` Available commands: @@ -99,6 +104,7 @@ Available commands: Key or k: Sends a key to the emulator Keys or ks: Sends a string to the emulator Return or r: Sends a return to the emulator + GIF or gif: Captures a GIF animation Help: Prints this help `) default: @@ -107,6 +113,40 @@ Available commands: } } +func SaveGif(a *izapple2.Apple2, filename string) error { + animation := gif.GIF{} + + delay := 50 * time.Millisecond + delayHundredsS := 5 + frames := 20 // 1 second + + planned := time.Now() + for i := 0; i < frames; i++ { + lapse := planned.Sub(time.Now()) + fmt.Printf("%v\n", lapse) + if lapse > 0 { + time.Sleep(lapse) + } + + fmt.Printf("%v\n", time.Now()) + img := screen.SnapshotPaletted(a, screen.ScreenModeNTSC) + animation.Image = append(animation.Image, img) + animation.Delay = append(animation.Delay, delayHundredsS) + + planned = planned.Add(delay) + } + + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + gif.EncodeAll(f, &animation) + return nil + +} + /* Uses the console to send commands and queries to an emulated machine. */ diff --git a/screen/ntscFilter.go b/screen/ntscFilter.go index 438b989..5f93fa9 100644 --- a/screen/ntscFilter.go +++ b/screen/ntscFilter.go @@ -30,6 +30,21 @@ var ntscColorMap = [16]color.Color{ color.RGBA{255, 255, 255, 255}, // White } +var attenuatedColorMap = buildAttenuatedColorMap(ntscColorMap) + +func buildAttenuatedColorMap(colorMap [16]color.Color) [16]color.Color { + colors := [16]color.Color{} + for i := 0; i < len(colorMap); i++ { + r, g, b, _ := colorMap[i].RGBA() + colors[i] = color.RGBA64{ + uint16(r / 2), uint16(g / 2), uint16(b / 2), + 65535, + } + } + return colors +} + +/* var rgbColorMap = [16]color.Color{ color.RGBA{0, 0, 0, 255}, // Black color.RGBA{221, 0, 51, 255}, // Magenta @@ -48,18 +63,13 @@ var rgbColorMap = [16]color.Color{ color.RGBA{68, 255, 153, 255}, // Aquamarine color.RGBA{255, 255, 255, 255}, // White } +*/ func filterNTSCColor(in *image.RGBA, mask *image.Alpha, screenMode int) *image.RGBA { colorMap := ntscColorMap // or rgbColorMap - attenuatedColorMap := ntscColorMap + colorMapLow := ntscColorMap if screenMode == ScreenModeNTSC { - for i := 0; i < len(colorMap); i++ { - r, g, b, _ := colorMap[i].RGBA() - attenuatedColorMap[i] = color.RGBA64{ - uint16(r / 2), uint16(g / 2), uint16(b / 2), - 65535, - } - } + colorMapLow = attenuatedColorMap } b := in.Bounds() @@ -90,7 +100,7 @@ func filterNTSCColor(in *image.RGBA, mask *image.Alpha, screenMode int) *image.R if r != 0 { cOut = colorMap[v] } else { - cOut = attenuatedColorMap[v] + cOut = colorMapLow[v] } if mask != nil { // RGB mode7 diff --git a/screen/snapshots.go b/screen/snapshots.go index a417152..6b7855e 100644 --- a/screen/snapshots.go +++ b/screen/snapshots.go @@ -36,6 +36,16 @@ func Snapshot(vs VideoSource, screenMode int) *image.RGBA { return snap } +// SnapshotPaletted, snapshot of the currently visible screen as a paletted image +func SnapshotPaletted(vs VideoSource, screenMode int) *image.Paletted { + img := Snapshot(vs, screenMode) + return palletedFilter(img) +} + +// Color for typical Apple ][ period green P1 phosphor monitors +// See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays +var greenPhosphorColor = color.RGBA{65, 255, 0, 255} + func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGBA { videoBase := videoMode & VideoBaseMask mixMode := videoMode & VideoMixTextMask @@ -44,9 +54,7 @@ func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGB var lightColor color.Color = color.White if screenMode == ScreenModeGreen { - // Color for typical Apple ][ period green P1 phosphor monitors - // See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays - lightColor = color.RGBA{65, 255, 0, 255} + lightColor = greenPhosphorColor } applyNTSCFilter := screenMode != ScreenModeGreen @@ -170,3 +178,21 @@ func linesSeparatedFilter(in *image.RGBA) *image.RGBA { } return out } + +func palletedFilter(in *image.RGBA) *image.Paletted { + bounds := in.Bounds() + outBounds := image.Rect(0, 0, bounds.Dx()*2, bounds.Dy()) + palette := []color.Color{color.Black, color.White, greenPhosphorColor} + palette = append(palette, ntscColorMap[:]...) + palette = append(palette, attenuatedColorMap[:]...) + paletted := image.NewPaletted(outBounds, palette) + + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + c := in.At(x, y) + paletted.Set(x*2, y, c) + paletted.Set(x*2+1, y, c) + } + } + return paletted +}