2019-04-21 21:04:02 +02:00
|
|
|
package apple2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"image/color"
|
|
|
|
"image/png"
|
|
|
|
"os"
|
|
|
|
)
|
|
|
|
|
2019-05-02 12:21:07 +02:00
|
|
|
/*
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
const (
|
2020-08-09 17:42:47 +02:00
|
|
|
// Base Video Mode
|
|
|
|
videoBaseMask uint16 = 0x1f
|
|
|
|
videoText40 uint16 = 0x01
|
|
|
|
videoGR uint16 = 0x02
|
|
|
|
videoHGR uint16 = 0x03
|
|
|
|
videoText80 uint16 = 0x08
|
|
|
|
videoDGR uint16 = 0x09
|
|
|
|
videoDHGR uint16 = 0x0a
|
|
|
|
videoText40RGB uint16 = 0x10
|
|
|
|
videoMono560 uint16 = 0x11
|
|
|
|
videoRGBMix uint16 = 0x12
|
|
|
|
videoRGB160 uint16 = 0x13
|
|
|
|
videoSHR uint16 = 0x14
|
|
|
|
|
|
|
|
// Mix text modifiers
|
|
|
|
videoMixTextMask uint16 = 0x0f00
|
|
|
|
videoMixText40 uint16 = 0x0100
|
|
|
|
videoMixText80 uint16 = 0x0200
|
|
|
|
videoMixText40RGB uint16 = 0x0300
|
|
|
|
|
|
|
|
// Other modifiers
|
|
|
|
videoModifiersMask uint16 = 0xf000
|
|
|
|
videoSecondPage uint16 = 0x1000
|
2020-08-08 13:44:45 +02:00
|
|
|
)
|
2020-07-31 09:46:53 +02:00
|
|
|
|
2020-08-09 17:42:47 +02:00
|
|
|
func getCurrentVideoMode(a *Apple2) uint16 {
|
2019-04-26 18:08:30 +02:00
|
|
|
isTextMode := a.io.isSoftSwitchActive(ioFlagText)
|
|
|
|
isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes)
|
2019-11-08 23:56:54 +01:00
|
|
|
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
|
2020-08-09 17:42:47 +02:00
|
|
|
isStore80Active := a.mmu.store80Active
|
2019-11-09 17:44:04 +01:00
|
|
|
isDoubleResMode := !isTextMode && is80Columns && !a.io.isSoftSwitchActive(ioFlagAnnunciator3)
|
2019-11-11 22:58:42 +01:00
|
|
|
isSuperHighResMode := a.io.isSoftSwitchActive(ioDataNewVideo)
|
2019-05-12 19:22:32 +02:00
|
|
|
|
2020-09-23 18:08:19 +02:00
|
|
|
isRGBCard := a.io.isSoftSwitchActive(ioFlagRGBCardActive)
|
2020-08-06 18:35:34 +02:00
|
|
|
rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard)
|
|
|
|
rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard)
|
|
|
|
isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2
|
|
|
|
isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2
|
2020-08-09 16:42:16 +02:00
|
|
|
isRGB160Mode := isDoubleResMode && rgbFlag1 && !rgbFlag2
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
isMixMode := a.io.isSoftSwitchActive(ioFlagMixed)
|
|
|
|
|
2020-08-09 17:42:47 +02:00
|
|
|
mode := uint16(0)
|
2020-08-08 13:44:45 +02:00
|
|
|
if isSuperHighResMode {
|
|
|
|
mode = videoSHR
|
|
|
|
isMixMode = false
|
|
|
|
} else if isTextMode {
|
|
|
|
if is80Columns {
|
|
|
|
mode = videoText80
|
2020-09-23 18:08:19 +02:00
|
|
|
} else if isRGBCard && isStore80Active {
|
2020-08-09 17:42:47 +02:00
|
|
|
mode = videoText40RGB
|
2020-08-08 13:44:45 +02:00
|
|
|
} else {
|
2020-08-09 17:42:47 +02:00
|
|
|
mode = videoText40
|
2020-08-08 13:44:45 +02:00
|
|
|
}
|
|
|
|
isMixMode = false
|
|
|
|
} else if isHiResMode {
|
|
|
|
if !isDoubleResMode {
|
|
|
|
mode = videoHGR
|
|
|
|
} else if isMono560 {
|
|
|
|
mode = videoMono560
|
|
|
|
} else if isRGBMixMode {
|
|
|
|
mode = videoRGBMix
|
2020-08-09 16:42:16 +02:00
|
|
|
} else if isRGB160Mode {
|
|
|
|
mode = videoRGB160
|
2020-08-08 13:44:45 +02:00
|
|
|
} else {
|
|
|
|
mode = videoDHGR
|
|
|
|
}
|
|
|
|
} else if isDoubleResMode {
|
|
|
|
mode = videoDGR
|
|
|
|
} else {
|
|
|
|
mode = videoGR
|
|
|
|
}
|
|
|
|
|
|
|
|
// Modifiers
|
|
|
|
if isMixMode {
|
|
|
|
if is80Columns {
|
|
|
|
mode |= videoMixText80
|
2020-08-11 23:13:12 +02:00
|
|
|
} else /* if isStore80Active {
|
2020-08-09 17:42:47 +02:00
|
|
|
mode |= videoMixText40RGB
|
2020-08-11 23:13:12 +02:00
|
|
|
} else */{
|
2020-08-08 13:44:45 +02:00
|
|
|
mode |= videoMixText40
|
|
|
|
}
|
|
|
|
}
|
|
|
|
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
|
|
|
|
if isSecondPage {
|
|
|
|
mode |= videoSecondPage
|
|
|
|
}
|
|
|
|
|
|
|
|
return mode
|
|
|
|
}
|
|
|
|
|
2020-08-09 17:42:47 +02:00
|
|
|
func snapshotByMode(a *Apple2, videoMode uint16) *image.RGBA {
|
2020-08-08 13:44:45 +02:00
|
|
|
videoBase := videoMode & videoBaseMask
|
2020-08-09 17:42:47 +02:00
|
|
|
mixMode := videoMode & videoMixTextMask
|
2020-08-08 13:44:45 +02:00
|
|
|
isSecondPage := (videoMode & videoSecondPage) != 0
|
2020-08-06 18:35:34 +02:00
|
|
|
|
2019-05-05 13:25:45 +02:00
|
|
|
var lightColor color.Color
|
2020-08-08 13:44:45 +02:00
|
|
|
if a.isColor {
|
2019-05-05 13:25:45 +02:00
|
|
|
lightColor = color.White
|
|
|
|
} else {
|
|
|
|
// Color for typical Apple ][ period green P1 phosphor monitors
|
2019-05-03 21:45:29 +02:00
|
|
|
// See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays
|
2019-05-05 13:25:45 +02:00
|
|
|
lightColor = color.RGBA{65, 255, 0, 255}
|
|
|
|
|
|
|
|
}
|
2019-05-03 21:45:29 +02:00
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
applyNTSCFilter := a.isColor
|
2019-05-05 13:25:45 +02:00
|
|
|
var snap *image.RGBA
|
2020-08-06 18:35:34 +02:00
|
|
|
var ntscMask *image.Alpha
|
2020-08-08 13:44:45 +02:00
|
|
|
switch videoBase {
|
|
|
|
case videoText40:
|
2020-08-08 19:23:35 +02:00
|
|
|
snap = snapshotText40Mode(a, isSecondPage, lightColor)
|
2020-08-08 13:44:45 +02:00
|
|
|
applyNTSCFilter = false
|
|
|
|
case videoText80:
|
2020-08-08 19:23:35 +02:00
|
|
|
snap = snapshotText80Mode(a, isSecondPage, lightColor)
|
|
|
|
applyNTSCFilter = false
|
2020-08-09 17:42:47 +02:00
|
|
|
case videoText40RGB:
|
2020-08-08 19:23:35 +02:00
|
|
|
snap = snapshotText40RGBMode(a, isSecondPage)
|
2020-08-08 13:44:45 +02:00
|
|
|
applyNTSCFilter = false
|
|
|
|
case videoGR:
|
2020-08-08 19:23:35 +02:00
|
|
|
snap = snapshotLoResModeMono(a, isSecondPage, lightColor)
|
2020-08-08 13:44:45 +02:00
|
|
|
case videoDGR:
|
2020-08-08 19:23:35 +02:00
|
|
|
snap = snapshotMeResModeMono(a, isSecondPage, lightColor)
|
2020-08-08 13:44:45 +02:00
|
|
|
case videoHGR:
|
|
|
|
snap = snapshotHiResModeMono(a, isSecondPage, lightColor)
|
|
|
|
case videoDHGR:
|
|
|
|
snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor)
|
|
|
|
case videoMono560:
|
|
|
|
snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor)
|
|
|
|
applyNTSCFilter = false
|
|
|
|
case videoRGBMix:
|
|
|
|
snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/, lightColor)
|
2020-08-09 16:42:16 +02:00
|
|
|
case videoRGB160:
|
|
|
|
snap = snapshotDoubleHiRes160ModeMono(a, isSecondPage, lightColor)
|
2020-08-08 13:44:45 +02:00
|
|
|
case videoSHR:
|
2019-11-11 22:58:42 +01:00
|
|
|
snap = snapshotSuperHiResMode(a)
|
2020-08-08 13:44:45 +02:00
|
|
|
applyNTSCFilter = false
|
|
|
|
}
|
2019-05-13 00:17:31 +02:00
|
|
|
|
2020-08-09 17:42:47 +02:00
|
|
|
if applyNTSCFilter {
|
|
|
|
snap = filterNTSCColor(snap, ntscMask)
|
|
|
|
}
|
|
|
|
|
|
|
|
if mixMode != 0 {
|
2020-08-08 19:23:35 +02:00
|
|
|
var bottom *image.RGBA
|
2020-08-09 17:42:47 +02:00
|
|
|
applyNTSCFilter := a.isColor
|
|
|
|
switch mixMode {
|
|
|
|
case videoMixText40:
|
2020-08-08 19:23:35 +02:00
|
|
|
bottom = snapshotText40Mode(a, isSecondPage, lightColor)
|
2020-08-09 17:42:47 +02:00
|
|
|
case videoMixText80:
|
2020-08-08 19:23:35 +02:00
|
|
|
bottom = snapshotText80Mode(a, isSecondPage, lightColor)
|
2020-08-09 17:42:47 +02:00
|
|
|
case videoMixText40RGB:
|
|
|
|
bottom = snapshotText40RGBMode(a, isSecondPage)
|
|
|
|
applyNTSCFilter = false
|
|
|
|
}
|
|
|
|
if applyNTSCFilter {
|
2020-08-11 23:13:12 +02:00
|
|
|
bottom = filterNTSCColor(bottom, ntscMask)
|
2020-08-08 19:23:35 +02:00
|
|
|
}
|
2020-08-08 13:44:45 +02:00
|
|
|
snap = mixSnapshots(snap, bottom)
|
2019-05-03 21:45:29 +02:00
|
|
|
}
|
2019-05-05 13:25:45 +02:00
|
|
|
|
2019-05-03 21:45:29 +02:00
|
|
|
return snap
|
|
|
|
}
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
// Snapshot the currently visible screen
|
|
|
|
func (a *Apple2) Snapshot() *image.RGBA {
|
|
|
|
videoMode := getCurrentVideoMode(a)
|
|
|
|
snap := snapshotByMode(a, videoMode)
|
2020-07-31 09:46:53 +02:00
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
if snap.Bounds().Dy() == hiResHeight {
|
|
|
|
// Apply the filter to regular CRT snapshots with 192 lines. Not to SHR
|
|
|
|
snap = linesSeparatedFilter(snap)
|
2020-07-31 09:46:53 +02:00
|
|
|
}
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
return snap
|
2020-07-31 09:46:53 +02:00
|
|
|
}
|
|
|
|
|
2019-05-03 21:45:29 +02:00
|
|
|
func mixSnapshots(top, bottom *image.RGBA) *image.RGBA {
|
2020-08-08 13:44:45 +02:00
|
|
|
topWidth := top.Bounds().Dx()
|
|
|
|
bottomWidth := bottom.Bounds().Dx()
|
2019-05-03 21:45:29 +02:00
|
|
|
factor := topWidth / bottomWidth
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
// Copy bottom's bottom on top's bottom, applying the factor
|
|
|
|
for y := hiResHeightMixed; y < hiResHeight; y++ {
|
|
|
|
for x := 0; x < topWidth; x++ {
|
2019-05-03 21:45:29 +02:00
|
|
|
c := bottom.At(x, y)
|
|
|
|
for f := 0; f < factor; f++ {
|
2020-08-08 13:44:45 +02:00
|
|
|
top.Set(x*factor+f, y, c)
|
2019-05-03 21:45:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-08 13:44:45 +02:00
|
|
|
return top
|
2019-04-21 21:04:02 +02:00
|
|
|
}
|
|
|
|
|
2019-06-02 22:59:51 +02:00
|
|
|
// SaveSnapshot saves a snapshot of the screen to a png file
|
2019-10-06 01:26:00 +02:00
|
|
|
func SaveSnapshot(a *Apple2, filename string) error {
|
2020-08-08 13:44:45 +02:00
|
|
|
img := a.Snapshot()
|
2019-06-02 22:59:51 +02:00
|
|
|
img = squarishPixelsFilter(img)
|
2019-04-21 21:04:02 +02:00
|
|
|
|
2019-06-02 22:59:51 +02:00
|
|
|
f, err := os.Create(filename)
|
2019-04-21 21:04:02 +02:00
|
|
|
if err != nil {
|
2019-10-06 01:26:00 +02:00
|
|
|
return err
|
2019-04-21 21:04:02 +02:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
png.Encode(f, img)
|
2019-10-06 01:26:00 +02:00
|
|
|
return nil
|
2019-04-21 21:04:02 +02:00
|
|
|
}
|
|
|
|
|
2019-06-02 22:59:51 +02:00
|
|
|
func squarishPixelsFilter(in *image.RGBA) *image.RGBA {
|
|
|
|
b := in.Bounds()
|
|
|
|
factor := 1200 / b.Dx()
|
|
|
|
fmt.Println(factor)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-05-01 16:54:53 +02:00
|
|
|
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
|
|
|
|
}
|