Files
8bitMyGoUI/fx/scenes_test.go
T
ZacharyZhang-NY 8be66c09f2 Add fx components 291-320 and the home scene; Kit.Shot, Kit.Toward
Palette cycling, sky, sun and moon, clouds, starfield, rain, snow,
petals, leaves, fireflies, waterfall, ocean, campfire, lava, aurora,
lightning, mountains and a season scene; scanlines, vignette, screen
shake, flash, dither fades, iris wipes, dissolves, mosaics, glitches,
sparkles, explosions and confetti. Home opens on a live scene in the
palette's season and a strip of all eight palettes.

pixel adds Canvas.Cycled, Sprite.At, SpriteOf, Kit.Toward and
Kit.Shot, which spends a cue under Reduce Motion so it never replays;
display exports Reduce. Gallery grids and the sidebar are one Tab
stop each.
2026-10-12 04:05:25 -04:00

253 lines
8.5 KiB
Go

package fx_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/fx"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestPaletteCycleRotatesTheBand(t *testing.T) {
band := []pixel.Slot{pixel.Accent, pixel.Good, pixel.Info}
sp := pixel.MustSprite("agcr")
clk := newClock()
tt := view(8, 2, pixel.SpringDay, clk, false, func(c *ui.Context) {
fx.PaletteCycle(c, sp, band, fx.PaletteCycleOptions{})
})
for range 4 {
n, d := clk.step(8), dots(tt.Image(), pixel.SpringDay, 4, 1)
for i := range band {
if want := band[(i+n)%3]; d[0][i] != int(want) {
t.Fatalf("step %d: dot %d is %d, want %v", n, i, d[0][i], want)
}
}
if d[0][3] != int(pixel.Bad) {
t.Fatal("a slot outside the band changed")
}
clk.t = clk.t.Add(125 * time.Millisecond)
tt.Frame()
}
mustPanic(t, "one slot", func() {
view(8, 2, pixel.SpringDay, clk, false, func(c *ui.Context) { fx.PaletteCycle(c, sp, band[:1], fx.PaletteCycleOptions{}) })
})
}
func TestSkyBandsByDayStarsByNight(t *testing.T) {
d := dots(view(80, 48, pixel.SpringDay, newClock(), false, func(c *ui.Context) {
fx.Sky(c, fx.SkyOptions{Width: 80, Height: 48})
}).Image(), pixel.SpringDay, 80, 48)
// By day the top band is plain Info: no stars.
if count(d, 0, 0, 80, 24, pixel.Info) != 80*24 || count(d, 0, 33, 80, 1, pixel.AccentLit) != 80 || count(d, 0, 47, 80, 1, pixel.Lit) != 80 {
t.Fatal("the day sky must run Info, AccentLit, Lit")
}
n := dots(view(80, 48, pixel.WinterNight, newClock(), false, func(c *ui.Context) {
fx.Sky(c, fx.SkyOptions{Width: 80, Height: 48})
}).Image(), pixel.WinterNight, 80, 48)
if count(n, 0, 0, 80, 32, pixel.Ink) == 0 || count(n, 0, 32, 80, 16, pixel.Ink) != 0 {
t.Fatal("stars belong to the top two thirds of the night")
}
}
func TestSunMoonRidesItsArc(t *testing.T) {
at := func(hour, x, y int) int {
d := dots(view(160, 96, pixel.SpringDay, newClock(), false, func(c *ui.Context) {
fx.SunMoon(c, hour, fx.SunMoonOptions{})
}).Image(), pixel.SpringDay, 160, 96)
if hour >= 6 && hour < 18 == (count(d, 0, 0, 160, 96, pixel.Warn) == 0) {
t.Fatalf("hour %d: the sun is out by day only", hour)
}
return d[y][x]
}
// Noon and midnight top the arc; six sits low left.
for _, tc := range []struct {
hour, x, y int
want pixel.Slot
}{{12, 85, 10, pixel.Warn}, {6, 15, 63, pixel.Warn}, {0, 85, 10, pixel.Lit}, {18, 15, 63, pixel.Lit}} {
if got := at(tc.hour, tc.x, tc.y); got != int(tc.want) {
t.Errorf("hour %d: dot %d,%d is %d, want %v", tc.hour, tc.x, tc.y, got, tc.want)
}
}
for _, hour := range []int{-1, 24} {
mustPanic(t, "hour", func() {
view(16, 16, pixel.SpringDay, newClock(), false, func(c *ui.Context) { fx.SunMoon(c, hour, fx.SunMoonOptions{}) })
})
}
}
// ridge is each column's top s dot, or the height.
func ridge(d [][]int, s pixel.Slot) []int {
out := make([]int, len(d[0]))
for x := range out {
out[x] = len(d)
for y := range d {
if d[y][x] == int(s) {
out[x] = y
break
}
}
}
return out
}
func TestMountainRidgesScrollAtTheirSpeeds(t *testing.T) {
clk := newClock()
tt := view(80, 48, pixel.SpringDay, clk, false, func(c *ui.Context) {
fx.Mountains(c, fx.MountainsOptions{Width: 80, Height: 48})
})
near, far := ridge(dots(tt.Image(), pixel.SpringDay, 80, 48), pixel.Edge), ridge(dots(tt.Image(), pixel.SpringDay, 80, 48), pixel.Shade)
// The near ridge moves every two steps, the far eight.
clk.t = clk.t.Add(250 * time.Millisecond)
tt.Frame()
if now := ridge(dots(tt.Image(), pixel.SpringDay, 80, 48), pixel.Edge); !equalShift(near, now, -1) {
t.Fatalf("near ridge %v became %v", near, now)
}
clk.t = clk.t.Add(750 * time.Millisecond)
tt.Frame()
if now := ridge(dots(tt.Image(), pixel.SpringDay, 80, 48), pixel.Shade); !equalShift(far, now, 48) {
t.Fatalf("far ridge %v became %v", far, now)
}
}
// equalShift is whether b is a, one column left.
func equalShift(a, b []int, hidden int) bool {
shown := 0
for x := range len(a) - 1 {
if b[x] == hidden || a[x+1] == hidden {
continue
}
if b[x] != a[x+1] {
return false
}
shown++
}
return shown > len(a)/2
}
func TestOceanCrestsLightUpInTurn(t *testing.T) {
clk := newClock()
tt := view(80, 48, pixel.SpringDay, clk, false, func(c *ui.Context) { fx.Ocean(c, fx.OceanOptions{Width: 80, Height: 48}) })
// The first crest row runs Info, Lit, AccentLit, Info.
for range 4 {
d, n := dots(tt.Image(), pixel.SpringDay, 80, 48), clk.step(6)
lit, pink := count(d, 0, 3, 80, 1, pixel.Lit), count(d, 0, 3, 80, 1, pixel.AccentLit)
if want := [4][2]bool{{false, false}, {true, false}, {false, true}, {false, false}}[n%4]; (lit > 0) != want[0] || (pink > 0) != want[1] {
t.Fatalf("step %d: %d Lit and %d AccentLit dots on the first crest", n, lit, pink)
}
clk.t = clk.t.Add(tick(6))
tt.Frame()
}
}
func TestWaterfallSegmentsFallThreeRowsAStep(t *testing.T) {
clk := newClock()
tt := view(40, 48, pixel.SpringDay, clk, false, func(c *ui.Context) {
fx.Waterfall(c, fx.WaterfallOptions{Width: 40, Height: 48})
})
before := dots(tt.Image(), pixel.SpringDay, 40, 48)
clk.t = clk.t.Add(100 * time.Millisecond)
tt.Frame()
after := dots(tt.Image(), pixel.SpringDay, 40, 48)
lit := 0
for x := 10; x < 30; x++ {
for y := 3; y < 39; y++ {
if before[y-3][x] == int(pixel.Lit) {
lit++
}
if after[y][x] != before[y-3][x] {
t.Fatalf("water at %d,%d did not fall three rows", x, y)
}
}
}
// One segment in six is lit.
if lit < 20*36/6-40 || lit > 20*36/6+40 {
t.Fatalf("%d lit water dots", lit)
}
}
func TestFireHeatRisesARowAStep(t *testing.T) {
clk := newClock()
tt := view(32, 40, pixel.SpringDay, clk, false, func(c *ui.Context) { fx.Fire(c, fx.FireOptions{}) })
for range 4 {
d, n, glows := dots(tt.Image(), pixel.SpringDay, 32, 40), clk.step(10), 0
for y := range d {
for x := range d[y] {
if d[y][x] == int(pixel.Lit) {
glows++
if row := y/2 - 2; (row+n)%4 != 0 {
t.Fatalf("step %d: heat glows on flame row %d", n, row)
}
}
}
}
if glows == 0 {
t.Fatalf("step %d: no heat", n)
}
clk.t = clk.t.Add(100 * time.Millisecond)
tt.Frame()
}
}
func TestSeasonScenePaintsInItsSeasonsPalette(t *testing.T) {
for _, tc := range []struct {
season pixel.Season
hour int
pal *pixel.Palette
}{{pixel.Spring, 10, pixel.SpringDay}, {pixel.Summer, 21, pixel.SummerNight}, {pixel.Autumn, 5, pixel.AutumnNight}, {pixel.Winter, 6, pixel.WinterDay}} {
d := dots(view(80, 48, pixel.SpringDay, newClock(), false, func(c *ui.Context) {
fx.SeasonScene(c, tc.season, tc.hour, fx.SeasonSceneOptions{Width: 80, Height: 48})
}).Image(), tc.pal, 80, 48)
for y := range d {
for x := range d[y] {
if d[y][x] < 0 {
t.Fatalf("%s at %d: dot %d,%d is off its palette", tc.pal.Name, tc.hour, x, y)
}
}
}
}
mustPanic(t, "season", func() {
view(16, 16, pixel.SpringDay, newClock(), false, func(c *ui.Context) { fx.SeasonScene(c, 4, 10, fx.SeasonSceneOptions{}) })
})
mustPanic(t, "hour", func() {
view(16, 16, pixel.SpringDay, newClock(), false, func(c *ui.Context) { fx.SeasonScene(c, pixel.Spring, 24, fx.SeasonSceneOptions{}) })
})
}
func TestWeatherKeepsToItsSlots(t *testing.T) {
for name, tc := range map[string]struct {
build func(c *ui.Context)
slots []pixel.Slot
}{
"Rain": {func(c *ui.Context) { fx.Rain(c, fx.RainOptions{Width: 80, Height: 48}) }, []pixel.Slot{pixel.Faint}},
"Snow": {func(c *ui.Context) { fx.Snow(c, fx.SnowOptions{Width: 80, Height: 48}) }, []pixel.Slot{pixel.Lit}},
"Petals": {func(c *ui.Context) { fx.Petals(c, fx.PetalsOptions{Width: 80, Height: 48}) }, []pixel.Slot{pixel.Accent, pixel.AccentLit}},
"Leaves": {func(c *ui.Context) { fx.Leaves(c, fx.LeavesOptions{Width: 80, Height: 48}) }, []pixel.Slot{pixel.Warn, pixel.Bad, pixel.Accent}},
"Fireflies": {func(c *ui.Context) { fx.Fireflies(c, fx.FirefliesOptions{Width: 80, Height: 48}) }, []pixel.Slot{pixel.Good, pixel.Warn}},
"Confetti": {func(c *ui.Context) { fx.Confetti(c, fx.ConfettiOptions{Width: 80, Height: 48}) },
[]pixel.Slot{pixel.Accent, pixel.Good, pixel.Warn, pixel.Bad, pixel.Info, pixel.Magic}},
} {
d := dots(view(80, 48, pixel.SpringDay, newClock(), false, tc.build).Image(), pixel.SpringDay, 80, 48)
seen := 0
for y := range d {
for x, s := range d[y] {
if s == int(pixel.Back) {
continue
}
seen++
ok := false
for _, want := range tc.slots {
ok = ok || s == int(want)
}
if !ok {
t.Fatalf("%s drew slot %d at %d,%d", name, s, x, y)
}
}
}
if seen == 0 {
t.Fatalf("%s drew nothing", name)
}
}
}