Files
8bitMyGoUI/pixel/motion_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

156 lines
5.2 KiB
Go

package pixel_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestCycledRemapsTheBand(t *testing.T) {
sp := pixel.MustSprite("agcr")
band := []pixel.Slot{pixel.Accent, pixel.Good, pixel.Info}
for _, tc := range []struct {
draw func(g pixel.Canvas) pixel.Canvas
want []pixel.Slot
}{
{func(g pixel.Canvas) pixel.Canvas { return g.Cycled(band, 1) }, []pixel.Slot{pixel.Good, pixel.Info, pixel.Accent, pixel.Bad}},
{func(g pixel.Canvas) pixel.Canvas { return g.Cycled(band, -1) }, []pixel.Slot{pixel.Info, pixel.Accent, pixel.Good, pixel.Bad}},
// The later band maps first, the earlier one after it.
{func(g pixel.Canvas) pixel.Canvas {
return g.Cycled(band[:2], 1).Cycled(band[1:], 1)
}, []pixel.Slot{pixel.Good, pixel.Info, pixel.Accent, pixel.Bad}},
} {
d := canvas(t, 4, 1, func(g pixel.Canvas) { tc.draw(g).Sprite(0, 0, sp, 0, pixel.Ink, 1) })
for i, want := range tc.want {
if got := d.at(i, 0); got != want {
t.Fatalf("dot %d drew %v, want %v", i, got, want)
}
}
}
// The original canvas keeps its slots.
d := canvas(t, 2, 1, func(g pixel.Canvas) {
g.Cycled(band, 1)
g.Sprite(0, 0, sp, 0, pixel.Ink, 1)
})
if d.at(0, 0) != pixel.Accent {
t.Fatal("Cycled changed the canvas it came from")
}
for name, band := range map[string][]pixel.Slot{"repeat": {pixel.Good, pixel.Good}, "overflow": {pixel.Good, pixel.Slots}, "empty": nil} {
mustPanic(t, name, func() { canvas(t, 1, 1, func(g pixel.Canvas) { g.Cycled(band, 1) }) })
}
}
func TestSpriteAtAndSpriteOf(t *testing.T) {
sp := pixel.MustSprite("a#.\ng.y\n\nyyy\nyyy")
for _, tc := range []struct {
frame, x, y int
want pixel.Slot
ok bool
}{{0, 0, 0, pixel.Accent, true}, {0, 1, 0, pixel.Warn, true}, {0, 2, 0, 0, false}, {0, 2, 1, pixel.Warn, true}, {1, 1, 1, pixel.Warn, true}, {2, 0, 1, pixel.Good, true}} {
if got, ok := sp.At(tc.frame, tc.x, tc.y, pixel.Warn); got != tc.want || ok != tc.ok {
t.Fatalf("At(%d, %d, %d) = %v %v, want %v %v", tc.frame, tc.x, tc.y, got, ok, tc.want, tc.ok)
}
}
mustPanic(t, "At outside", func() { sp.At(0, 3, 0, pixel.Ink) })
of := pixel.SpriteOf(3, 1, func(x, y int) (pixel.Slot, bool) { return pixel.Slot(x + 11), x != 1 })
d := canvas(t, 3, 1, func(g pixel.Canvas) { g.Sprite(0, 0, of, 0, pixel.Ink, 1) })
if d.at(0, 0) != pixel.Good || d.at(1, 0) != pixel.Back || d.at(2, 0) != pixel.Bad {
t.Fatal("SpriteOf drew the wrong dots")
}
mustPanic(t, "SpriteOf empty", func() { pixel.SpriteOf(0, 1, func(x, y int) (pixel.Slot, bool) { return 0, true }) })
mustPanic(t, "SpriteOf slot", func() { pixel.SpriteOf(1, 1, func(x, y int) (pixel.Slot, bool) { return pixel.Slots, true }) })
}
func TestShotSpendsCuesUnderReduceMotion(t *testing.T) {
at := time.Unix(1_700_000_000, 0)
cue, step := 0, -1
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
step = pixel.K(c).Shot(c, ui.Box(c), cue, 10, 5)
}, 10, 10)
frame := func(want int) {
t.Helper()
tt.Frame()
if step != want {
t.Fatalf("cue %d: step %d, want %d", cue, step, want)
}
}
// No cue yet: the shot is over.
frame(5)
cue = 1
frame(0)
at = at.Add(200 * time.Millisecond)
frame(2)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
cue = 2
frame(5)
tt.SetPreferences(ui.Preferences{ReduceMotion: false, TextScale: 1})
frame(5)
cue = 3
frame(0)
}
func TestTowardSettlesUnderReduceMotion(t *testing.T) {
at := time.Unix(1_700_000_000, 0)
on, step := false, -1
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
step = pixel.K(c).Toward(c, ui.Box(c), on, 8, 4)
}, 10, 10)
on = true
tt.Frame()
at = at.Add(125 * time.Millisecond)
tt.Frame()
// Reduce Motion jumps to the end, and the end stays.
for _, reduce := range []bool{true, false} {
tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1})
tt.Frame()
if step != 4 {
t.Fatalf("reduce %v: step %d, want 4", reduce, step)
}
}
}
func TestTowardStepsAndTurnsWhereItIs(t *testing.T) {
for _, reduce := range []bool{false, true} {
at := time.Unix(1_700_000_000, 0)
on, step := true, -1
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
e := ui.Box(c)
step = pixel.K(c).Toward(c, e, on, 8, 4)
}, 10, 10)
if reduce {
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
}
frame := func(d time.Duration, want int) {
t.Helper()
at = at.Add(d)
tt.Frame()
if !reduce && step != want || reduce && step != 4*boolInt(on) {
t.Fatalf("reduce %v, on %v: step %d, want %d", reduce, on, step, want)
}
}
// Shown first, it is already at its end.
frame(0, 4)
on = false
frame(0, 4)
frame(125*time.Millisecond, 3)
frame(125*time.Millisecond, 2)
// Turned at 2, it climbs back from 2.
on = true
frame(0, 2)
frame(125*time.Millisecond, 3)
frame(time.Second, 4)
}
mustPanic(t, "no steps", func() {
ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
pixel.K(c).Toward(c, ui.Box(c), true, 8, 0)
}, 10, 10)
})
}