Files
8bitMyGoUI/media/pictures_test.go
T
ZacharyZhang-NY 0e21870b64 Add media components 346-365; input.SlideBase, guarded Pad
An audio player with transport, scrubber and volume; an equalizer and
a visualizer; a playlist; a cassette, cartridge and record that turn
while they play and hold where they stop; piano keys, a step sequencer
and a tracker you can type into; a waveform, a 16:9 video frame, an
image compare, a gallery, a filmstrip, a record button and an FM tuner.

input exports SlideBase, the undrawn rail its sliders share; the
gallery treats exported Bases as building blocks, not components.
pixel's micro font gains '#' for notes like C#4. pixel.Pad and the
slide rail read input only while their base is enabled, so Inert or a
fresh Disabled stops a drag or a key already sent. AGENTS records why
a drawn container keeps the DISABLED description: MyGo paints a
disabled subtree at half opacity.
2026-10-12 06:17:12 -04:00

187 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package media_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/media"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
// step is one frame at 8 fps, plus a hair.
const step = time.Second/8 + time.Microsecond
func TestCassetteReelsTurnOnlyWhilePlaying(t *testing.T) {
for _, tc := range []struct {
name string
playing, reduce bool
turns bool
}{
{"stopped", false, false, false}, {"playing", true, false, true}, {"reduced", true, true, false},
} {
wd := newWorld()
wd.reduce = tc.reduce
tt := view(300, 200, wd, func(c *ui.Context) {
media.Cassette(c, "Tape", media.CassetteOptions{Title: "Side A", Playing: tc.playing})
})
x, y, _, _ := dots(find(t, tt, "Tape"))
// The left hub's spokes stand + then lean ×.
plus := func() bool {
img := tt.Image()
return at(img, wd.pal, x+24, y+20) == pixel.Ink && at(img, wd.pal, x+24, y+21) == pixel.Lit
}
if !plus() {
t.Fatalf("%s: the hub does not start upright", tc.name)
}
later(tt, wd, step)
if plus() == tc.turns {
t.Fatalf("%s: turned %v, want %v", tc.name, !plus(), tc.turns)
}
}
}
func TestCassetteTapeCrossesOver(t *testing.T) {
for _, tc := range []struct {
progress float64
left, right pixel.Slot
}{{0, pixel.Faint, pixel.Ink}, {1, pixel.Ink, pixel.Faint}} {
wd := newWorld()
tt := view(300, 200, wd, func(c *ui.Context) {
media.Cassette(c, "Tape", media.CassetteOptions{Progress: tc.progress})
})
x, y, _, _ := dots(find(t, tt, "Tape"))
img := tt.Image()
// A full pack reaches five dots out; an empty, three.
if l, r := at(img, wd.pal, x+18, y+20), at(img, wd.pal, x+45, y+20); l != tc.left || r != tc.right {
t.Fatalf("progress %v: left %v, right %v; want %v, %v", tc.progress, l, r, tc.left, tc.right)
}
}
for name, o := range map[string]media.CassetteOptions{
"progress past the end": {Progress: 1.5},
"a title in hanzi": {Title: "混音"},
"a title past the label": {Title: "THE LONGEST MIXTAPE"},
} {
mustPanic(t, name, func() {
view(300, 200, newWorld(), func(c *ui.Context) { media.Cassette(c, "Tape", o) })
})
}
}
func TestCartridgeStickerAndCorner(t *testing.T) {
wd := newWorld()
art := pixel.SpriteOf(4, 4, func(x, y int) (pixel.Slot, bool) { return pixel.Good, true })
tt := view(300, 300, wd, func(c *ui.Context) {
media.Cartridge(c, "Cart", media.CartridgeOptions{Title: "Pixel Quest", Art: art})
media.Cartridge(c, "Short", media.CartridgeOptions{Title: "Night Run"})
})
x, y, _, _ := dots(find(t, tt, "Cart"))
img := tt.Image()
if at(img, wd.pal, x+28, y+24) != pixel.Good {
t.Fatal("the art does not sit in the sticker's middle")
}
// PIXEL and QUEST take a line each.
if count(img, wd.pal, x+8, y+37, 40, 5, pixel.Ink) == 0 || count(img, wd.pal, x+8, y+43, 40, 5, pixel.Ink) == 0 {
t.Fatal("the title does not fill two lines")
}
if at(img, wd.pal, x+53, y) != pixel.Back || at(img, wd.pal, x+40, y) != pixel.Edge {
t.Fatal("the top right corner is not cut")
}
// One line sits in the middle of the band.
x, y, _, _ = dots(find(t, tt, "Short"))
if count(img, wd.pal, x+8, y+37, 40, 3, pixel.Ink) != 0 || count(img, wd.pal, x+8, y+40, 40, 5, pixel.Ink) == 0 {
t.Fatal("NIGHT RUN is not one middle line")
}
for name, o := range map[string]media.CartridgeOptions{
"art past the sticker": {Art: pixel.SpriteOf(40, 4, func(x, y int) (pixel.Slot, bool) { return pixel.Ink, true })},
"three lines of title": {Title: "ONE TWO THREE FOUR FIVE"},
"a word past a line": {Title: "SUPERCALIFRAGILISTIC"},
} {
mustPanic(t, name, func() {
view(300, 300, newWorld(), func(c *ui.Context) { media.Cartridge(c, "Cart", o) })
})
}
}
func TestVinylTurnsAndTheArmSwings(t *testing.T) {
wd := newWorld()
playing := false
tt := view(300, 200, wd, func(c *ui.Context) { media.Vinyl(c, "Record", media.VinylOptions{Playing: playing}) })
x, y, _, _ := dots(find(t, tt, "Record"))
lit := func(dx, dy int) bool { return at(tt.Image(), wd.pal, x+dx, y+dy) == pixel.AccentLit }
head := func(dx, dy int) bool { return at(tt.Image(), wd.pal, x+dx, y+dy) == pixel.Edge }
if !lit(36, 31) || !head(66, 36) {
t.Fatal("stopped, the mark is not at three o'clock or the arm not at rest")
}
later(tt, wd, step)
if !lit(36, 31) {
t.Fatal("a stopped record turned")
}
playing = true
tt.Frame()
later(tt, wd, step)
if !lit(35, 35) || lit(36, 31) {
t.Fatal("playing, the mark did not turn an eighth")
}
later(tt, wd, time.Second/2)
if !head(52, 32) || head(66, 36) {
t.Fatal("the arm did not swing onto the grooves")
}
// Five eighths round, it stops where it is.
playing = false
later(tt, wd, step)
if !lit(27, 27) || lit(36, 31) {
t.Fatal("a stopped record did not hold its turn")
}
}
func TestFilmstripHoldsEveryFrame(t *testing.T) {
wd := newWorld()
sp := pixel.MustSprite("g.\n.g\n\n.g\ng.\n\nr.\n.r")
tt := view(300, 200, wd, func(c *ui.Context) {
media.Filmstrip(c, "Film", sp, media.FilmstripOptions{Scale: 2})
})
x, y, w, h := dots(find(t, tt, "Film"))
// Frames are 8 wide, film 4 more; 20 tall.
if w != 3*8+4 || h != 6+14 {
t.Fatalf("film is %d by %d dots", w, h)
}
img := tt.Image()
if at(img, wd.pal, x+5, y+8) != pixel.Good || at(img, wd.pal, x+13, y+8) != pixel.Lit || at(img, wd.pal, x+21, y+8) != pixel.Bad {
t.Fatal("the frames are not in order")
}
if at(img, wd.pal, x+15, y+10) != pixel.Lit {
t.Fatal("a frame's backing is narrower than its sprite")
}
if at(img, wd.pal, x+2, y+2) != pixel.Faint || at(img, wd.pal, x+2, y+h-3) != pixel.Faint || at(img, wd.pal, x+5, y+2) != pixel.Ink {
t.Fatal("no sprocket holes")
}
}
func TestWaveformAndVisualizerFollowTheirData(t *testing.T) {
wd := newWorld()
tt := view(300, 200, wd, func(c *ui.Context) {
media.Waveform(c, "Wave", []float64{1, -0.5, 0}, media.WaveformOptions{Height: 21})
media.Visualizer(c, "Bars", []float64{1, 0.5, 0}, media.VisualizerOptions{Height: 20})
})
x, y, w, _ := dots(find(t, tt, "Wave"))
img := tt.Image()
// Up ten from the middle, down five, then the middle.
if w != 5 || at(img, wd.pal, x, y) != pixel.Accent || at(img, wd.pal, x+2, y+15) != pixel.Accent ||
at(img, wd.pal, x+2, y+16) == pixel.Accent || at(img, wd.pal, x+2, y+9) == pixel.Accent || at(img, wd.pal, x+4, y+10) != pixel.Accent {
t.Fatal("waveform lines do not follow the samples")
}
x, y, _, _ = dots(find(t, tt, "Bars"))
if at(img, wd.pal, x+1, y) != pixel.AccentLit || at(img, wd.pal, x+1, y+19) != pixel.AccentLit || at(img, wd.pal, x+5, y+5) != pixel.AccentLit ||
at(img, wd.pal, x+5, y+4) == pixel.AccentLit || count(img, wd.pal, x+8, y, 3, 20, pixel.Magic)+count(img, wd.pal, x+8, y, 3, 20, pixel.AccentLit) != 0 {
t.Fatal("visualizer bars do not reach their levels")
}
mustPanic(t, "a sample past 1", func() {
view(300, 200, newWorld(), func(c *ui.Context) { media.Waveform(c, "W", []float64{1.5}, media.WaveformOptions{}) })
})
mustPanic(t, "a level below 0", func() {
view(300, 200, newWorld(), func(c *ui.Context) { media.Visualizer(c, "V", []float64{-0.1}, media.VisualizerOptions{}) })
})
}