izapple2/screen/snapshots.go

201 lines
5.5 KiB
Go

package screen
import (
"image"
"image/color"
"image/png"
"os"
)
/*
References:
- "Understanding the Apple II", http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
- "Apple II Reference Manual"
- "More Colors for your Apple", https://archive.org/details/byte-magazine-1979-06/page/n61
*/
const (
// ScreenModeGreen to render as a green phosphor monitor
ScreenModeGreen = iota
// ScreenModePlain to render in color with filled areas
ScreenModePlain
//ScreenModeNTSC shows spaces between pixels
ScreenModeNTSC
)
// Snapshot the currently visible screen
func Snapshot(vs VideoSource, screenMode int) *image.RGBA {
videoMode := vs.GetCurrentVideoMode()
snap := snapshotByMode(vs, videoMode, screenMode)
if screenMode != ScreenModePlain && snap.Bounds().Dy() == hiResHeight {
// Apply the filter to regular CRT snapshots with 192 lines. Not to SHR
snap = linesSeparatedFilter(snap)
}
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
isSecondPage := (videoMode & VideoSecondPage) != 0
isAltText := (videoMode & VideoAltText) != 0
isRGBCard := (videoMode & VideoRGBCard) != 0
shiftSupported := (videoMode & VideoFourColors) == 0
var lightColor color.Color = color.White
if screenMode == ScreenModeGreen {
lightColor = greenPhosphorColor
}
applyNTSCFilter := screenMode != ScreenModeGreen
var snap *image.RGBA
var ntscMask *image.Alpha
switch videoBase {
case VideoText40:
snap = snapshotText40(vs, isSecondPage, isAltText, lightColor)
applyNTSCFilter = false
case VideoText80:
snap = snapshotText80(vs, isSecondPage, isAltText, lightColor)
applyNTSCFilter = false
case VideoText40RGB:
snap = snapshotText40RGB(vs, isSecondPage, isAltText)
applyNTSCFilter = false
case VideoGR:
snap = snapshotLoRes(vs, isSecondPage, lightColor)
case VideoDGR:
snap = snapshotMeRes(vs, isSecondPage, lightColor)
case VideoHGR:
snap = snapshotHiRes(vs, isSecondPage, lightColor, shiftSupported)
case VideoDHGR:
snap, _ = snapshotDoubleHiRes(vs, isSecondPage, false /*isRGBMixMode*/, lightColor)
case VideoMono560:
snap, _ = snapshotDoubleHiRes(vs, isSecondPage, false /*isRGBMixMode*/, lightColor)
applyNTSCFilter = false
case VideoRGBMix:
snap, ntscMask = snapshotDoubleHiRes(vs, isSecondPage, true /*isRGBMixMode*/, lightColor)
case VideoRGB160:
snap = snapshotDoubleHiRes160(vs, isSecondPage, lightColor)
case VideoSHR:
snap = snapshotSuperHiRes(vs)
applyNTSCFilter = false
case VideoVidex:
snap = vs.GetCardImage(lightColor)
applyNTSCFilter = false
}
if applyNTSCFilter {
snap = filterNTSCColor(snap, ntscMask, screenMode)
}
if mixMode != 0 {
var bottom *image.RGBA
applyNTSCFilter := screenMode != ScreenModeGreen && !isRGBCard
switch mixMode {
case VideoMixText40:
bottom = snapshotText40(vs, isSecondPage, isAltText, lightColor)
case VideoMixText80:
bottom = snapshotText80(vs, isSecondPage, isAltText, lightColor)
case VideoMixText40RGB:
bottom = snapshotText40RGB(vs, isSecondPage, isAltText)
applyNTSCFilter = false
}
if applyNTSCFilter {
bottom = filterNTSCColor(bottom, ntscMask, screenMode)
}
snap = mixSnapshots(snap, bottom)
}
return snap
}
func mixSnapshots(top, bottom *image.RGBA) *image.RGBA {
bottomWidth := bottom.Bounds().Dx()
// Copy bottom's bottom on top's bottom
for y := hiResHeightMixed; y < hiResHeight; y++ {
for x := 0; x < bottomWidth; x++ {
c := bottom.At(x, y)
top.Set(x, y, c)
}
}
return top
}
// SaveSnapshot saves a snapshot of the screen to a png file
func SaveSnapshot(vs VideoSource, screenMode int, filename string) error {
img := Snapshot(vs, screenMode)
img = squarishPixelsFilter(img)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
png.Encode(f, img)
return nil
}
func squarishPixelsFilter(in *image.RGBA) *image.RGBA {
b := in.Bounds()
factor := 1200 / b.Dx()
size := image.Rect(0, 0, factor*b.Dx(), b.Dy())
out := image.NewRGBA(size)
for x := b.Min.X; x < b.Max.X; x++ {
for y := b.Min.Y; y < b.Max.Y; y++ {
c := in.At(x, y)
for i := 0; i < factor; i++ {
out.Set(factor*x+i, y, c)
}
}
}
return out
}
func linesSeparatedFilter(in *image.RGBA) *image.RGBA {
b := in.Bounds()
size := image.Rect(0, 0, b.Dx(), 4*b.Dy())
out := image.NewRGBA(size)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := in.At(x, y)
out.Set(x, 4*y, c)
out.Set(x, 4*y+1, c)
out.Set(x, 4*y+2, c)
out.Set(x, 4*y+3, color.Black)
}
}
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
}