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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 04:05:25 -04:00
1 parent 86a6901333
commit 8be66c09f2
56 files changed
+3324 -101

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
package fx_test
import (
"image"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/fx"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type clock struct{ t time.Time }
func (c *clock) now() time.Time { return c.t }
func newClock() *clock { return &clock{time.Unix(1_800_000_000, 0)} }
// step is the clock's frame at fps, as pixel counts.
func (c *clock) step(fps int64) int { return int(c.t.UnixNano() / (int64(time.Second) / fps)) }
// tick is one step at fps, just past the edge.
func tick(fps int) time.Duration { return time.Second/time.Duration(fps) + time.Microsecond }
// view builds w×h dots on a fake clock, 2px each.
func view(w, h int, pal *pixel.Palette, clk *clock, reduce bool, build func(c *ui.Context)) *ui.Tester {
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pal, Now: clk.now})
build(c)
}, 2*w, 2*h)
if reduce {
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
tt.Frame()
}
return tt
}
// dots reads w×h dots as slots; -1 is off-palette.
func dots(img *image.RGBA, pal *pixel.Palette, w, h int) [][]int {
out := make([][]int, h)
for y := range h {
out[y] = make([]int, w)
for x := range w {
c, s := img.RGBAAt(2*x, 2*y), -1
for i, pc := range pal.Colors {
if pc.R == c.R && pc.G == c.G && pc.B == c.B {
s = i
break
}
}
out[y][x] = s
}
}
return out
}
// count is how many dots of a box hold s.
func count(d [][]int, x, y, w, h int, s pixel.Slot) int {
n := 0
for j := y; j < y+h; j++ {
for i := x; i < x+w; i++ {
if d[j][i] == int(s) {
n++
}
}
}
return n
}
func same(a, b [][]int) bool {
for y := range a {
for x := range a[y] {
if a[y][x] != b[y][x] {
return false
}
}
}
return true
}
func mustPanic(t *testing.T, name string, fn func()) {
t.Helper()
defer func() {
if recover() == nil {
t.Errorf("%s did not panic", name)
}
}()
fn()
}
// solid is a one-slot w×h box under effects.
func solid(c *ui.Context, w, h int, s pixel.Slot) {
k := pixel.K(c)
ui.Box(c).Size(k.D(w), k.D(h)).Label("content").Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, g.W, g.H, s)
})
}
var portal = pixel.MustSprite("agcagc\ncagcag")
func TestAmbientEffectsMoveButHoldStillUnderReduceMotion(t *testing.T) {
for name, build := range map[string]func(c *ui.Context){
"PaletteCycle": func(c *ui.Context) {
fx.PaletteCycle(c, portal, []pixel.Slot{pixel.Accent, pixel.Good, pixel.Info}, fx.PaletteCycleOptions{})
},
"SunMoon": func(c *ui.Context) { fx.SunMoon(c, 12, fx.SunMoonOptions{Width: 80, Height: 48}) },
"Clouds": func(c *ui.Context) { fx.Clouds(c, fx.CloudsOptions{Width: 80, Height: 48}) },
"Starfield": func(c *ui.Context) { fx.Starfield(c, fx.StarfieldOptions{Width: 80, Height: 48}) },
"Rain": func(c *ui.Context) { fx.Rain(c, fx.RainOptions{Width: 80, Height: 48}) },
"Snow": func(c *ui.Context) { fx.Snow(c, fx.SnowOptions{Width: 80, Height: 48}) },
"Petals": func(c *ui.Context) { fx.Petals(c, fx.PetalsOptions{Width: 80, Height: 48}) },
"Leaves": func(c *ui.Context) { fx.Leaves(c, fx.LeavesOptions{Width: 80, Height: 48}) },
"Fireflies": func(c *ui.Context) { fx.Fireflies(c, fx.FirefliesOptions{Width: 80, Height: 48}) },
"Waterfall": func(c *ui.Context) { fx.Waterfall(c, fx.WaterfallOptions{Width: 40, Height: 48}) },
"Ocean": func(c *ui.Context) { fx.Ocean(c, fx.OceanOptions{Width: 80, Height: 48}) },
"Fire": func(c *ui.Context) { fx.Fire(c, fx.FireOptions{}) },
"Lava": func(c *ui.Context) { fx.Lava(c, fx.LavaOptions{Width: 80, Height: 48}) },
"Aurora": func(c *ui.Context) { fx.Aurora(c, fx.AuroraOptions{Width: 80, Height: 48}) },
"Mountains": func(c *ui.Context) { fx.Mountains(c, fx.MountainsOptions{Width: 80, Height: 48}) },
"Season": func(c *ui.Context) { fx.SeasonScene(c, pixel.Winter, 22, fx.SeasonSceneOptions{Width: 80, Height: 48}) },
"Sparkle": func(c *ui.Context) { fx.Sparkle(c, fx.SparkleOptions{}) },
"Confetti": func(c *ui.Context) { fx.Confetti(c, fx.ConfettiOptions{Width: 80, Height: 48}) },
"NightSky": func(c *ui.Context) { fx.Sky(c, fx.SkyOptions{Width: 80, Height: 48}) },
"NightSnows": func(c *ui.Context) { fx.Snow(c, fx.SnowOptions{Width: 80, Height: 48}) },
} {
// A day sky holds still: only stars move.
pal := pixel.SpringDay
if name == "NightSky" || name == "NightSnows" || name == "Season" {
pal = pixel.WinterNight
}
for _, reduce := range []bool{false, true} {
clk := newClock()
tt := view(80, 48, pal, clk, reduce, build)
first := dots(tt.Image(), pal, 80, 48)
moved := false
for _, d := range []time.Duration{300, 700, 1300} {
clk.t = clk.t.Add(d * time.Millisecond)
tt.Frame()
moved = moved || !same(first, dots(tt.Image(), pal, 80, 48))
}
if moved == reduce {
t.Errorf("%s, reduce %v: moved %v", name, reduce, moved)
}
}
}
}