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:
56 files changed
+3324
-101
No files matched your search
@@ -15,6 +15,8 @@ type Canvas struct {
|
||||
k Kit
|
||||
gx, gy int
|
||||
W, H int
|
||||
// remap swaps slots as they draw; nil keeps them.
|
||||
remap *[Slots]Slot
|
||||
}
|
||||
|
||||
// painter is what Canvas needs of *ui.Painter.
|
||||
@@ -42,10 +44,35 @@ func (g Canvas) Rect(x, y, w, h int, s Slot) {
|
||||
if w <= 0 || h <= 0 {
|
||||
return
|
||||
}
|
||||
if g.remap != nil {
|
||||
s = g.remap[s]
|
||||
}
|
||||
d := g.k.dot
|
||||
g.p.Fill(ui.Rect{X: float32(g.gx+x) * d, Y: float32(g.gy+y) * d, W: float32(w) * d, H: float32(h) * d}, g.k.C(s), 0)
|
||||
}
|
||||
|
||||
// Cycled draws band's slots where Cycle(band, step) moves them.
|
||||
func (g Canvas) Cycled(band []Slot, step int) Canvas {
|
||||
var m [Slots]Slot
|
||||
for i := range m {
|
||||
m[i] = Slot(i)
|
||||
}
|
||||
to := Cycle(band, step)
|
||||
for i, s := range band {
|
||||
if s >= Slots || slices.Index(band, s) != i {
|
||||
panic(fmt.Sprintf("pixel: Cycled band %v repeats or overflows", band))
|
||||
}
|
||||
m[s] = to[i]
|
||||
}
|
||||
if g.remap != nil {
|
||||
for i := range m {
|
||||
m[i] = g.remap[m[i]]
|
||||
}
|
||||
}
|
||||
g.remap = &m
|
||||
return g
|
||||
}
|
||||
|
||||
// Clip limits fn's drawing to a dot rectangle.
|
||||
func (g Canvas) Clip(x, y, w, h int, fn func()) {
|
||||
d := g.k.dot
|
||||
|
||||
@@ -74,6 +74,71 @@ func (k Kit) Since(c *ui.Context, t time.Time, fps, done int) int {
|
||||
return min(n, done)
|
||||
}
|
||||
|
||||
type towardKey struct{}
|
||||
|
||||
// heading is a Toward's target, last turn and step then.
|
||||
type heading struct {
|
||||
on bool
|
||||
at time.Time
|
||||
from int
|
||||
}
|
||||
|
||||
// Toward steps 0–n toward on, turning from where it is.
|
||||
func (k Kit) Toward(c *ui.Context, e *ui.Element, on bool, fps, n int) int {
|
||||
checkFPS(fps)
|
||||
if n < 1 {
|
||||
panic(fmt.Sprintf("pixel: Toward needs steps, got %d", n))
|
||||
}
|
||||
end := func(on bool) int {
|
||||
if on {
|
||||
return n
|
||||
}
|
||||
return 0
|
||||
}
|
||||
// First shown, it already sits where on puts it.
|
||||
s := ui.Local(e, towardKey{}, func() heading { return heading{on: on, from: end(on)} })
|
||||
// Reduce Motion settles at the end, so nothing resumes later.
|
||||
if k.reduce {
|
||||
*s = heading{on: on, from: end(on)}
|
||||
return s.from
|
||||
}
|
||||
shown := func() int {
|
||||
if s.on {
|
||||
return s.from + k.Since(c, s.at, fps, n-s.from)
|
||||
}
|
||||
return s.from - k.Since(c, s.at, fps, s.from)
|
||||
}
|
||||
if on != s.on {
|
||||
s.from, s.on, s.at = shown(), on, k.now
|
||||
}
|
||||
return shown()
|
||||
}
|
||||
|
||||
type shotKey struct{}
|
||||
|
||||
// shot is a one-shot's cue, start, and spent mark.
|
||||
type shot struct {
|
||||
cue int
|
||||
at time.Time
|
||||
spent bool
|
||||
}
|
||||
|
||||
// Shot steps a cued one-shot; Reduce Motion spends it whole.
|
||||
func (k Kit) Shot(c *ui.Context, e *ui.Element, cue, fps, done int) int {
|
||||
// Before any change the zero start reads as done.
|
||||
s := ui.Local(e, shotKey{}, func() shot { return shot{cue: cue} })
|
||||
if cue != s.cue {
|
||||
s.cue, s.at, s.spent = cue, k.now, false
|
||||
}
|
||||
if k.reduce {
|
||||
s.spent = true
|
||||
}
|
||||
if s.spent {
|
||||
return done
|
||||
}
|
||||
return k.Since(c, s.at, fps, done)
|
||||
}
|
||||
|
||||
type cueKey struct{}
|
||||
|
||||
// cued is a cue's last value and when it changed.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -71,6 +71,42 @@ func cell(c byte) int8 {
|
||||
|
||||
func (s *Sprite) Frames() int { return len(s.frames) }
|
||||
|
||||
// At is frame's slot at x, y; false where clear.
|
||||
func (s *Sprite) At(frame, x, y int, ink Slot) (Slot, bool) {
|
||||
if x < 0 || y < 0 || x >= s.W || y >= s.H {
|
||||
panic(fmt.Sprintf("pixel: sprite dot %d,%d outside %d×%d", x, y, s.W, s.H))
|
||||
}
|
||||
n := len(s.frames)
|
||||
switch v := s.frames[(frame%n+n)%n][y*s.W+x]; v {
|
||||
case cellClear:
|
||||
return 0, false
|
||||
case cellInk:
|
||||
return ink, true
|
||||
default:
|
||||
return Slot(v), true
|
||||
}
|
||||
}
|
||||
|
||||
// SpriteOf builds a one-frame sprite; at reports each dot.
|
||||
func SpriteOf(w, h int, at func(x, y int) (Slot, bool)) *Sprite {
|
||||
if w < 1 || h < 1 {
|
||||
panic(fmt.Sprintf("pixel: sprite of %d×%d", w, h))
|
||||
}
|
||||
px := make([]int8, w*h)
|
||||
for y := range h {
|
||||
for x := range w {
|
||||
px[y*w+x] = cellClear
|
||||
if s, ok := at(x, y); ok {
|
||||
if s >= Slots {
|
||||
panic(fmt.Sprintf("pixel: sprite slot %d at %d,%d", s, x, y))
|
||||
}
|
||||
px[y*w+x] = int8(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &Sprite{W: w, H: h, frames: [][]int8{px}}
|
||||
}
|
||||
|
||||
// Mirror returns s flipped left to right.
|
||||
func (s *Sprite) Mirror() *Sprite {
|
||||
m := &Sprite{W: s.W, H: s.H}
|
||||
|
||||
Reference in new issue
Block a user