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
@@ -42,7 +42,7 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
- Name palette slots, never colors. Every pixel must be a palette color.
|
||||
- Only axis-aligned rectangles reach the screen: no radius, path, shadow, gradient, opacity, scaled bitmap.
|
||||
- Tone comes from Bayer dither between two slots.
|
||||
- Animation steps through `Canvas.Step`, `Canvas.Busy` or `Kit.Tick`. No easing.
|
||||
- Animation steps through `Canvas.Step`, `Canvas.Busy` or `Kit.Tick`. No easing. Color cycling turns a band of slots a place each step: `Canvas.Cycled` remaps a drawn picture's slots; procedural art indexes `pixel.Cycle(band, step)`, which may repeat a slot.
|
||||
- Text through the pixel font only.
|
||||
|
||||
## MyGo rules
|
||||
@@ -55,7 +55,7 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
|
||||
- Signatures and results: PRD §5.1. Zero options give the default look. Sizes in options are dots.
|
||||
- `k := pixel.K(c)` once per component; layout sizes through `k.D(n)`. Integer options default through `pixel.Or`.
|
||||
- Custom-drawn controls end their Draw with `g.Finish(st)`. Grids of cells moved by arrows use `pixel.Pad`; one-shot timing steps with `k.Since`; a view that changes at a known moment asks `k.At`.
|
||||
- Custom-drawn controls end their Draw with `g.Finish(st)`. Grids of cells moved by arrows use `pixel.Pad`; one-shot timing steps with `k.Since`; a decorative one-shot on a cue steps with `k.Shot`, which Reduce Motion spends whole; a view that changes at a known moment asks `k.At`; a two-way slide (bars, fades, wipes) steps with `k.Toward`, which turns from where it is.
|
||||
- A one-shot effect inside a lasting view starts when its cue changes (a `Cue` option read through `k.Cue`, or a value that rises), never on its first frame. A state screen (`ErrorState`, `SuccessState`) starts when it appears. Decorative motion jumps to its end under Reduce Motion; countdowns and loaders keep time.
|
||||
- Containers take content as a final `func()`; interactive results are `NameResult`.
|
||||
- A Clicked, Shortcut, Changed or drag during the build makes MyGo build the frame again, so what they change shows at once. State changed any other way after its element was built (`Pressed`, `Edit.Changed`, a drag's release) is read at paint time, or set on the element after its children.
|
||||
@@ -84,7 +84,7 @@ Each component adds one `Entry` in `gallery/demos_<category>.go`: ID, Name, Zh,
|
||||
|
||||
- `go vet ./... && go test ./...` before reporting done.
|
||||
- Behavior tests drive `ui.Tester` and assert values. Timed components take `pixel.Settings.Now` from a test clock. `Tester.Focused(label)` is also true when an ancestor holds the focus.
|
||||
- `gallery` checks every entry under every palette: only palette colors, whole dots. `PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariants` writes PNGs for visual review.
|
||||
- `gallery` checks every entry under every palette: only palette colors, whole dots. Demos that call `pixel.Scope`, and the home and entry pages, may show other palettes. `PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariants` writes PNGs for visual review.
|
||||
- Web: `npm install && npm test` in `web/` builds `web/dist` and runs Playwright on a local server; `BASE_URL=<url> npx playwright test` checks a deployed site. Tests read `window.__8bit` (`ready`, `stats()`, `texts()`, `page()`, `find(label)`, `log`).
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -18,23 +18,39 @@ func fill(w, h int, c color.Color) *image.RGBA {
|
||||
return img
|
||||
}
|
||||
|
||||
// dots are a sprite's slots, -1 where clear.
|
||||
func dots(sp *pixel.Sprite) []int {
|
||||
var out []int
|
||||
for y := range sp.H {
|
||||
for x := range sp.W {
|
||||
s, ok := sp.At(0, x, y, pixel.Ink)
|
||||
if !ok {
|
||||
out = append(out, -1)
|
||||
continue
|
||||
}
|
||||
out = append(out, int(s))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestReduce(t *testing.T) {
|
||||
pal := pixel.SpringDay
|
||||
a := pal.Colors[pixel.Accent]
|
||||
for _, s := range reduce(fill(8, 8, color.RGBA{a.R, a.G, a.B, 255}), 4, 4, pal) {
|
||||
if s != int8(pixel.Accent) {
|
||||
for _, s := range dots(Reduce(fill(8, 8, color.RGBA{a.R, a.G, a.B, 255}), 4, 4, pal)) {
|
||||
if s != int(pixel.Accent) {
|
||||
t.Fatalf("a flat Accent image gave slot %d", s)
|
||||
}
|
||||
}
|
||||
for _, s := range reduce(fill(4, 4, color.RGBA{}), 4, 4, pal) {
|
||||
for _, s := range dots(Reduce(fill(4, 4, color.RGBA{}), 4, 4, pal)) {
|
||||
if s != -1 {
|
||||
t.Fatal("a clear image drew")
|
||||
}
|
||||
}
|
||||
i, b := pal.Colors[pixel.Info], pal.Colors[pixel.Back]
|
||||
mid := color.RGBA{uint8((int(i.R) + int(b.R)) / 2), uint8((int(i.G) + int(b.G)) / 2), uint8((int(i.B) + int(b.B)) / 2), 255}
|
||||
seen := map[int8]int{}
|
||||
for _, s := range reduce(fill(16, 16, mid), 16, 16, pal) {
|
||||
seen := map[int]int{}
|
||||
for _, s := range dots(Reduce(fill(16, 16, mid), 16, 16, pal)) {
|
||||
seen[s]++
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
|
||||
+24
-41
@@ -20,7 +20,7 @@ type pictureCache struct {
|
||||
sum uint64
|
||||
pal *pixel.Palette
|
||||
w, h int
|
||||
dots []int8
|
||||
sp *pixel.Sprite
|
||||
}
|
||||
|
||||
// Picture reduces img to the palette with Bayer dither.
|
||||
@@ -30,55 +30,38 @@ func Picture(c *ui.Context, img image.Image, o PictureOptions) *ui.Element {
|
||||
w, h := pixel.Or("Picture W", o.W, b.Dx()), pixel.Or("Picture H", o.H, b.Dy())
|
||||
e := art(c, k, o.Label, w, h, nil)
|
||||
pc := ui.Local(e, pictureKey{}, func() pictureCache { return pictureCache{} })
|
||||
if sum := fingerprint(img); pc.dots == nil || pc.sum != sum || pc.pal != k.Palette() || pc.w != w || pc.h != h {
|
||||
*pc = pictureCache{sum, k.Palette(), w, h, reduce(img, w, h, k.Palette())}
|
||||
if sum := fingerprint(img); pc.sp == nil || pc.sum != sum || pc.pal != k.Palette() || pc.w != w || pc.h != h {
|
||||
*pc = pictureCache{sum, k.Palette(), w, h, Reduce(img, w, h, k.Palette())}
|
||||
}
|
||||
dots := pc.dots
|
||||
sp := pc.sp
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
for y := range h {
|
||||
for x := 0; x < w; {
|
||||
s, end := dots[y*w+x], x+1
|
||||
for end < w && dots[y*w+end] == s {
|
||||
end++
|
||||
}
|
||||
if s >= 0 {
|
||||
g.Rect(x, y, end-x, 1, pixel.Slot(s))
|
||||
}
|
||||
x = end
|
||||
}
|
||||
}
|
||||
k.Canvas(p, r).Sprite(0, 0, sp, 0, pixel.Ink, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// reduce maps img to w×h slots; -1 marks clear dots.
|
||||
func reduce(img image.Image, w, h int, pal *pixel.Palette) []int8 {
|
||||
// Reduce maps img onto pal as a dithered w×h sprite.
|
||||
func Reduce(img image.Image, w, h int, pal *pixel.Palette) *pixel.Sprite {
|
||||
b := img.Bounds()
|
||||
out := make([]int8, w*h)
|
||||
for y := range h {
|
||||
return pixel.SpriteOf(w, h, func(x, y int) (pixel.Slot, bool) {
|
||||
y0, y1 := span(b.Min.Y, b.Dy(), y, h)
|
||||
for x := range w {
|
||||
x0, x1 := span(b.Min.X, b.Dx(), x, w)
|
||||
var sum [4]uint32
|
||||
n := uint32(0)
|
||||
for sy := y0; sy < y1; sy++ {
|
||||
for sx := x0; sx < x1; sx++ {
|
||||
r, g, bl, a := img.At(sx, sy).RGBA()
|
||||
sum[0], sum[1], sum[2], sum[3] = sum[0]+r>>8, sum[1]+g>>8, sum[2]+bl>>8, sum[3]+a>>8
|
||||
n++
|
||||
}
|
||||
x0, x1 := span(b.Min.X, b.Dx(), x, w)
|
||||
var sum [4]uint32
|
||||
n := uint32(0)
|
||||
for sy := y0; sy < y1; sy++ {
|
||||
for sx := x0; sx < x1; sx++ {
|
||||
r, g, bl, a := img.At(sx, sy).RGBA()
|
||||
sum[0], sum[1], sum[2], sum[3] = sum[0]+r>>8, sum[1]+g>>8, sum[2]+bl>>8, sum[3]+a>>8
|
||||
n++
|
||||
}
|
||||
if sum[3]/n < 128 {
|
||||
out[y*w+x] = -1
|
||||
continue
|
||||
}
|
||||
nudge := (float64(pixel.Bayer(x, y))+0.5)/16 - 0.5
|
||||
out[y*w+x] = int8(nearest(pal, [3]float64{
|
||||
float64(sum[0]/n) + nudge*48, float64(sum[1]/n) + nudge*48, float64(sum[2]/n) + nudge*48,
|
||||
}))
|
||||
}
|
||||
}
|
||||
return out
|
||||
if sum[3]/n < 128 {
|
||||
return 0, false
|
||||
}
|
||||
nudge := (float64(pixel.Bayer(x, y))+0.5)/16 - 0.5
|
||||
return nearest(pal, [3]float64{
|
||||
float64(sum[0]/n) + nudge*48, float64(sum[1]/n) + nudge*48, float64(sum[2]/n) + nudge*48,
|
||||
}), true
|
||||
})
|
||||
}
|
||||
|
||||
// fingerprint mixes img's bounds and pixels, to notice edits.
|
||||
|
||||
@@ -278,6 +278,24 @@ func TestReadyGoReportsDoneAfterAJump(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShakeSpendsCuesUnderReduceMotion(t *testing.T) {
|
||||
clk, cue := newClock(), 0
|
||||
tt := view(300, 100, clk, false, func(c *ui.Context) {
|
||||
feedback.Shake(c, feedback.ShakeOptions{Cue: cue}, func() {
|
||||
ui.Box(c).Size(20, 20).Label("box")
|
||||
})
|
||||
})
|
||||
x := find(t, tt, "box").X
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
cue++
|
||||
tt.Frame()
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: false, TextScale: 1})
|
||||
tt.Frame()
|
||||
if find(t, tt, "box").X != x {
|
||||
t.Fatal("a cue spent under Reduce Motion shook later")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShakeJoltsUnlessReduced(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
clk := newClock()
|
||||
|
||||
+1
-4
@@ -18,9 +18,6 @@ var shakes = [...]int{2, -2, 2, -2, 1, -1, 0}
|
||||
func Shake(c *ui.Context, o ShakeOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
e := ui.Box(c)
|
||||
dx := 0
|
||||
if step := k.Since(c, k.Cue(e, o.Cue), 20, len(shakes)-1); !k.Reduce() {
|
||||
dx = shakes[step]
|
||||
}
|
||||
dx := shakes[k.Shot(c, e, o.Cue, 20, len(shakes)-1)]
|
||||
return e.Padding(0, k.D(2-dx), 0, k.D(2+dx)).Children(content)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type AuroraOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Aurora hangs curtains whose slots slide sideways.
|
||||
func Aurora(c *ui.Context, o AuroraOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Aurora", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { aurora(g, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
// aurora's columns ripple on top and fade at the hem.
|
||||
func aurora(g pixel.Canvas, step int) {
|
||||
band := pixel.Cycle([]pixel.Slot{pixel.Good, pixel.Info, pixel.Magic, pixel.Good, pixel.AccentLit}, step)
|
||||
for x := range g.W {
|
||||
top := g.H/8 + tri(x/2+step, 64, g.H/6) + tri(x+2*step, 36, 3)
|
||||
long := g.H/4 + tri(3*x/2-step, 50, g.H/6)
|
||||
s := band[x/4%len(band)]
|
||||
g.VLine(x, top, long*2/3, s)
|
||||
g.Veil(x, top+long*2/3, 1, long/3, s, 6)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type CloudsOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Clouds drift by: far ones slowly, near ones faster.
|
||||
func Clouds(c *ui.Context, o CloudsOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Clouds", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { clouds(g, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
// clouds drifts two layers across the top third of g.
|
||||
func clouds(g pixel.Canvas, step int) {
|
||||
for layer, sp := range []*pixel.Sprite{smallCloud, bigCloud} {
|
||||
// Far clouds shift a dot each four steps; near, two.
|
||||
shift, span := step/(4>>layer), g.W+sp.W
|
||||
for i := range max(1, g.W/(120-20*layer)) {
|
||||
x := wrap(rnd(i, 10+layer)-shift, span) - sp.W
|
||||
y := rnd(i, 12+layer)%max(1, g.H/3) + 4*layer
|
||||
g.Sprite(x, y, sp, 0, pixel.Ink, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type ConfettiOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// confettiSlots color the bits in turn.
|
||||
var confettiSlots = []pixel.Slot{pixel.Accent, pixel.Good, pixel.Warn, pixel.Bad, pixel.Info, pixel.Magic}
|
||||
|
||||
// Confetti tumbles bits of every color down, turning.
|
||||
func Confetti(c *ui.Context, o ConfettiOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Confetti", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { confetti(g, g.Step(10)) })
|
||||
})
|
||||
}
|
||||
|
||||
// confetti turns each bit flat, edge-on, upright, edge-on.
|
||||
func confetti(g pixel.Canvas, step int) {
|
||||
for i := range g.W * g.H / 90 {
|
||||
speed := 1 + rnd(i, 90)%2
|
||||
y := wrap(rnd(i, 91)+step*speed, g.H+4) - 2
|
||||
x := wrap(rnd(i, 92)+sway(step+rnd(i, 93), 12, 1), g.W)
|
||||
s := confettiSlots[i%len(confettiSlots)]
|
||||
switch wrap(step+i, 4) {
|
||||
case 0:
|
||||
g.Rect(x, y, 2, 1, s)
|
||||
case 2:
|
||||
g.Rect(x, y, 1, 2, s)
|
||||
default:
|
||||
g.Dot(x, y, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type DissolveOptions struct {
|
||||
// Slot replaces the dots; zero means the darkest slot.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// Dissolve switches content's dots to a slot in shuffled order.
|
||||
func Dissolve(c *ui.Context, covered bool, o DissolveOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, dark(k))
|
||||
e := ui.Column(c)
|
||||
step := k.Toward(c, e, covered, 12, 8)
|
||||
return cover(k, e, func(g pixel.Canvas) { dissolve(g, s, step) }, content)
|
||||
}
|
||||
|
||||
// dissolve covers dots whose fixed rank, 0–7, is below step.
|
||||
func dissolve(g pixel.Canvas, s pixel.Slot, step int) {
|
||||
on := func(x, y int) bool { return rnd(x+y<<16, 150)%8 < step }
|
||||
for y := range g.H {
|
||||
for x := 0; x < g.W; x++ {
|
||||
if !on(x, y) {
|
||||
continue
|
||||
}
|
||||
end := x + 1
|
||||
for end < g.W && on(end, y) {
|
||||
end++
|
||||
}
|
||||
g.HLine(x, y, end-x, s)
|
||||
x = end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type ExplosionOptions struct {
|
||||
// Cue sets off a blast each time it changes.
|
||||
Cue int
|
||||
// Size is the square's side in dots; zero means 48.
|
||||
Size int
|
||||
Label string
|
||||
}
|
||||
|
||||
const blastSteps = 8
|
||||
|
||||
// debrisSlots color the bits thrown out, in turn.
|
||||
var debrisSlots = []pixel.Slot{pixel.Warn, pixel.Bad, pixel.Faint}
|
||||
|
||||
// Explosion bursts, breaks into smoke and throws debris.
|
||||
func Explosion(c *ui.Context, o ExplosionOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("Explosion Size", o.Size, 48)
|
||||
e := box(c, k, o.Label, n, n)
|
||||
step := k.Shot(c, e, o.Cue, 12, blastSteps)
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if step == blastSteps {
|
||||
return
|
||||
}
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { blast(g, k, step, o.Cue) })
|
||||
})
|
||||
}
|
||||
|
||||
// blast sizes rings and puffs in sixteenths of the box.
|
||||
func blast(g pixel.Canvas, k pixel.Kit, step, seed int) {
|
||||
cx, cy, u := g.W/2, g.H/2, max(1, g.W/16)
|
||||
hot := light(k)
|
||||
switch step {
|
||||
case 0:
|
||||
g.Disc(cx, cy, 2*u, pixel.Warn)
|
||||
g.Disc(cx, cy, u, hot)
|
||||
case 1, 2:
|
||||
g.Disc(cx, cy, (3+step)*u, pixel.Bad)
|
||||
g.Disc(cx, cy, 3*u, pixel.Warn)
|
||||
g.Disc(cx, cy, (3-step)*u, hot)
|
||||
default:
|
||||
// The fireball breaks into puffs that rise and thin out.
|
||||
for i := range 6 {
|
||||
a := float64(i)*math.Pi/3 + float64(rnd(seed, 180+i)%40-20)*math.Pi/180
|
||||
d := float64((step + 1) * u)
|
||||
x, y := cx+int(d*math.Cos(a)), cy+int(d*math.Sin(a))-(step-3)*u
|
||||
if r := max(1, (8-step)*u/2); step < 6 {
|
||||
g.Disc(x, y, r, pixel.Faint)
|
||||
} else {
|
||||
g.Circle(x, y, r, pixel.Faint)
|
||||
}
|
||||
}
|
||||
if step < 5 {
|
||||
g.Disc(cx, cy, (5-step)*u, pixel.Bad)
|
||||
g.Disc(cx, cy, (4-step)*u, pixel.Warn)
|
||||
}
|
||||
}
|
||||
// Debris flies out and falls.
|
||||
t := float64(step)
|
||||
for i := range 10 {
|
||||
a := float64(rnd(seed*16+i, 181)%360) * math.Pi / 180
|
||||
v := float64(u) * (1.5 + float64(rnd(seed*16+i, 182)%3)/2)
|
||||
x := cx + int(v*t*math.Cos(a))
|
||||
y := cy + int(v*t*math.Sin(a)+0.4*float64(u)*t*t)
|
||||
g.Rect(x, y, max(1, u/2), max(1, u/2), debrisSlots[i%len(debrisSlots)])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type FadeDitherOptions struct {
|
||||
// Slot covers the content; zero means the darkest slot.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// FadeDither covers content through Bayer levels, or uncovers it.
|
||||
func FadeDither(c *ui.Context, covered bool, o FadeDitherOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, dark(k))
|
||||
e := ui.Column(c)
|
||||
level := 2 * k.Toward(c, e, covered, 12, 8)
|
||||
return cover(k, e, func(g pixel.Canvas) { g.Veil(0, 0, g.W, g.H, s, level) }, content)
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type FireOptions struct {
|
||||
// Scale multiplies the 16×20 campfire; zero means 2.
|
||||
Scale int
|
||||
Label string
|
||||
}
|
||||
|
||||
// tongues are each flicker frame's three tongue heights.
|
||||
var tongues = [3][3]int{{3, 6, 2}, {4, 5, 3}, {2, 7, 4}}
|
||||
|
||||
// bases are the flame's half widths from row 6 down.
|
||||
var bases = [8]int{3, 4, 5, 5, 6, 6, 5, 4}
|
||||
|
||||
// flames are flicker frames: a Bad rim, a Warn body.
|
||||
var flames = func() (out [3]*pixel.Sprite) {
|
||||
for f := range out {
|
||||
in := func(x, y int) bool {
|
||||
if y < 0 || y > 13 || x < 0 || x > 12 {
|
||||
return false
|
||||
}
|
||||
if y >= 6 && abs(x-6) <= bases[y-6] {
|
||||
return true
|
||||
}
|
||||
for i, h := range tongues[f] {
|
||||
if tip := 6 - h; y >= tip && y <= 6 && abs(x-3-3*i) <= (y-tip)/2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
out[f] = pixel.SpriteOf(13, 14, func(x, y int) (pixel.Slot, bool) {
|
||||
switch {
|
||||
case !in(x, y):
|
||||
return 0, false
|
||||
case !in(x-1, y) || !in(x+1, y) || !in(x, y-1) || !in(x, y+1):
|
||||
return pixel.Bad, true
|
||||
}
|
||||
return pixel.Warn, true
|
||||
})
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
// logs cross under the fire; s marks the cut ends.
|
||||
var logs = pixel.MustSprite(`
|
||||
ss............ss
|
||||
s###........###s
|
||||
..####....####..
|
||||
....########....
|
||||
..####....####..
|
||||
s###........###s
|
||||
`)
|
||||
|
||||
// Fire burns a campfire: flicker frames, heat rising through it.
|
||||
func Fire(c *ui.Context, o FireOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
sc := scale("Fire", o.Scale, 2)
|
||||
return box(c, k, o.Label, 16*sc, 20*sc).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
fire(g, k, sc, g.Step(10))
|
||||
})
|
||||
}
|
||||
|
||||
// fire lights a third of every fourth body row, rising.
|
||||
func fire(g pixel.Canvas, k pixel.Kit, sc, step int) {
|
||||
px := func(x, y, w, h int, s pixel.Slot) { g.Rect(x*sc, y*sc, w*sc, h*sc, s) }
|
||||
wood := g
|
||||
if k.Palette().Night {
|
||||
wood = g.Cycled([]pixel.Slot{pixel.Shade, pixel.Lit}, 1)
|
||||
}
|
||||
wood.Sprite(0, 14*sc, logs, 0, pick(k, pixel.Faint, pixel.Face), sc)
|
||||
sp, hot := flames[wrap(step, 3)], nightLit(g, k)
|
||||
hot.Sprite(sc, 2*sc, sp, 0, pixel.Ink, sc)
|
||||
// Body rows index the band; turning it lifts the heat.
|
||||
heat := pixel.Cycle([]pixel.Slot{pixel.Lit, pixel.Warn, pixel.Warn, pixel.Warn}, step)
|
||||
for y := range sp.H {
|
||||
for x := range sp.W {
|
||||
if s, ok := sp.At(0, x, y, pixel.Ink); ok && s == pixel.Warn && rnd(16*y+x, 200)%3 == 0 && heat[y%4] == pixel.Lit {
|
||||
hot.Rect((1+x)*sc, (2+y)*sc, sc, sc, pixel.Lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sparks rise beside the tips.
|
||||
for i := range 2 {
|
||||
if t := wrap(step+5*i, 8); t < 5 {
|
||||
px(4+8*i+sway(step+3*i, 8, 1), 5-t, 1, 1, pixel.Warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type FirefliesOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Fireflies wander and glow: bright, dim, then dark a while.
|
||||
func Fireflies(c *ui.Context, o FirefliesOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Fireflies", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { fireflies(g, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
func fireflies(g pixel.Canvas, step int) {
|
||||
for i := range max(3, g.W*g.H/600) {
|
||||
x := rnd(i, 70)%g.W + sway(step+rnd(i, 71), 40+rnd(i, 72)%24, 8)
|
||||
y := rnd(i, 73)%g.H + sway(step+rnd(i, 74), 28+rnd(i, 75)%20, 5)
|
||||
switch t := wrap(step+rnd(i, 76), 16); {
|
||||
case t < 6:
|
||||
g.Rect(x-1, y, 3, 1, pixel.Good)
|
||||
g.Rect(x, y-1, 1, 3, pixel.Good)
|
||||
g.Dot(x, y, pixel.Warn)
|
||||
case t < 9:
|
||||
g.Dot(x, y, pixel.Good)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type FlashOptions struct {
|
||||
// Cue flashes each time it changes.
|
||||
Cue int
|
||||
// Slot fills the flash; zero means Lit.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// Flash fills content with a slot one step, then half.
|
||||
func Flash(c *ui.Context, o FlashOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, pixel.Lit)
|
||||
e := ui.Column(c)
|
||||
// Reduce Motion never flashes.
|
||||
step := k.Shot(c, e, o.Cue, 10, 2)
|
||||
return cover(k, e, func(g pixel.Canvas) {
|
||||
switch step {
|
||||
case 0:
|
||||
g.Rect(0, 0, g.W, g.H, s)
|
||||
case 1:
|
||||
g.Veil(0, 0, g.W, g.H, s, 8)
|
||||
}
|
||||
}, content)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Package fx draws weather, scenes and transitions in stepped frames.
|
||||
package fx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
// box sizes an effect; a label makes it an image.
|
||||
func box(c *ui.Context, k pixel.Kit, label string, w, h int) *ui.Element {
|
||||
e := ui.Box(c).Size(k.D(w), k.D(h))
|
||||
if label != "" {
|
||||
return e.Label(label).Role(ui.RoleImage)
|
||||
}
|
||||
return e.Role(ui.RoleNone)
|
||||
}
|
||||
|
||||
// size is w×h; zeros take dw×dh.
|
||||
func size(name string, w, h, dw, dh int) (int, int) {
|
||||
return pixel.Or(name+" Width", w, dw), pixel.Or(name+" Height", h, dh)
|
||||
}
|
||||
|
||||
// scale is s, or def for zero; above 4 panics.
|
||||
func scale(name string, s, def int) int {
|
||||
s = pixel.Or(name+" Scale", s, def)
|
||||
if s > 4 {
|
||||
panic(fmt.Sprintf("fx: %s Scale %d outside 1–4", name, s))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// rnd is a fixed pseudo-random number for i and salt.
|
||||
func rnd(i, salt int) int {
|
||||
x := uint64(i)*0x9E3779B97F4A7C15 + uint64(salt)*0xD1B54A32D192ED03
|
||||
x = (x ^ x>>30) * 0xBF58476D1CE4E5B9
|
||||
x = (x ^ x>>27) * 0x94D049BB133111EB
|
||||
return int((x ^ x>>31) >> 1)
|
||||
}
|
||||
|
||||
// wrap is v modulo n, never negative.
|
||||
func wrap(v, n int) int { return (v%n + n) % n }
|
||||
|
||||
// sway swings 0 to a, back through -a, over p.
|
||||
func sway(t, p, a int) int {
|
||||
t = wrap(t, p)
|
||||
q := p / 4
|
||||
switch {
|
||||
case t < q:
|
||||
return a * t / q
|
||||
case t < 3*q:
|
||||
return a - 2*a*(t-q)/(2*q)
|
||||
default:
|
||||
return -a + a*(t-3*q)/q
|
||||
}
|
||||
}
|
||||
|
||||
// pick is day under day palettes, night under night ones.
|
||||
func pick(k pixel.Kit, day, night pixel.Slot) pixel.Slot {
|
||||
if k.Palette().Night {
|
||||
return night
|
||||
}
|
||||
return day
|
||||
}
|
||||
|
||||
// light is the palette's brightest slot.
|
||||
func light(k pixel.Kit) pixel.Slot { return pick(k, pixel.Lit, pixel.Ink) }
|
||||
|
||||
// dark is the palette's darkest slot.
|
||||
func dark(k pixel.Kit) pixel.Slot { return pick(k, pixel.Ink, pixel.Well) }
|
||||
|
||||
// slotOr is s, or def for the zero slot.
|
||||
func slotOr(s, def pixel.Slot) pixel.Slot {
|
||||
if s == pixel.Ink {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// checkHour panics outside 0–23.
|
||||
func checkHour(name string, hour int) {
|
||||
if hour < 0 || hour > 23 {
|
||||
panic(fmt.Sprintf("fx: %s hour %d outside 0–23", name, hour))
|
||||
}
|
||||
}
|
||||
|
||||
// cover draws over e's content, clipped to e.
|
||||
func cover(k pixel.Kit, e *ui.Element, draw func(g pixel.Canvas), content func()) *ui.Element {
|
||||
return e.DrawOver(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { draw(g) })
|
||||
}).Children(content)
|
||||
}
|
||||
+148
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type GlitchOptions struct {
|
||||
// Cue glitches the picture each time it changes.
|
||||
Cue int
|
||||
// Slot inks the sprite's # dots.
|
||||
Slot pixel.Slot
|
||||
Scale int
|
||||
Label string
|
||||
}
|
||||
|
||||
const glitchSteps = 6
|
||||
|
||||
// Glitch shifts row bands and swaps two slots, briefly.
|
||||
func Glitch(c *ui.Context, sp *pixel.Sprite, o GlitchOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
sc := scale("Glitch", o.Scale, 1)
|
||||
// Three dots either side hold the rows shifted out.
|
||||
e := box(c, k, o.Label, (sp.W+6)*sc, sp.H*sc)
|
||||
step := k.Shot(c, e, o.Cue, 15, glitchSteps)
|
||||
used := slotsOf(sp, o.Slot)
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
if step == glitchSteps {
|
||||
g.Sprite(3*sc, 0, sp, 0, o.Slot, sc)
|
||||
return
|
||||
}
|
||||
seed := o.Cue*glitchSteps + step
|
||||
if len(used) > 1 {
|
||||
a := rnd(seed, 160) % len(used)
|
||||
b := (a + 1 + rnd(seed, 161)%(len(used)-1)) % len(used)
|
||||
g = g.Cycled([]pixel.Slot{used[a], used[b]}, 1)
|
||||
}
|
||||
for y := 0; y < sp.H; {
|
||||
h, dx := 1+rnd(seed+y, 162)%4, 0
|
||||
if rnd(seed+y, 163)%3 == 0 {
|
||||
dx = rnd(seed+y, 164)%7 - 3
|
||||
}
|
||||
g.Clip(0, y*sc, g.W, h*sc, func() { g.Sprite((3+dx)*sc, 0, sp, 0, o.Slot, sc) })
|
||||
y += h
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// slotsOf lists the slots sp's first frame uses.
|
||||
func slotsOf(sp *pixel.Sprite, ink pixel.Slot) []pixel.Slot {
|
||||
var seen [pixel.Slots]bool
|
||||
var out []pixel.Slot
|
||||
for y := range sp.H {
|
||||
for x := range sp.W {
|
||||
if s, ok := sp.At(0, x, y, ink); ok && !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type IrisWipeOptions struct {
|
||||
// Slot fills outside the iris; zero means the darkest slot.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// IrisWipe closes a circle on content's middle, or opens it.
|
||||
func IrisWipe(c *ui.Context, closed bool, o IrisWipeOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, dark(k))
|
||||
e := ui.Column(c)
|
||||
step := k.Toward(c, e, closed, 12, 8)
|
||||
return cover(k, e, func(g pixel.Canvas) { iris(g, s, step) }, content)
|
||||
}
|
||||
|
||||
// iris fills outside a circle, gone by step 8.
|
||||
func iris(g pixel.Canvas, s pixel.Slot, step int) {
|
||||
if step == 0 {
|
||||
return
|
||||
}
|
||||
cx, cy := g.W/2, g.H/2
|
||||
r := int(math.Ceil(math.Hypot(float64(g.W)/2, float64(g.H)/2))) * (8 - step) / 8
|
||||
for y := range g.H {
|
||||
dy := y - cy
|
||||
if dy*dy >= r*r {
|
||||
g.HLine(0, y, g.W, s)
|
||||
continue
|
||||
}
|
||||
dx := int(math.Sqrt(float64(r*r - dy*dy)))
|
||||
g.HLine(0, y, cx-dx, s)
|
||||
g.HLine(cx+dx, y, g.W-cx-dx, s)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type LavaOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 48.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Lava flows in cycling veins while bubbles swell and pop.
|
||||
func Lava(c *ui.Context, o LavaOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Lava", o.Width, o.Height, 160, 48)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { lava(g, k, g.Step(6)) })
|
||||
})
|
||||
}
|
||||
|
||||
// lava's veins flow and cycle row by row; crust drifts.
|
||||
func lava(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
g.Rect(0, 0, g.W, g.H, pixel.Bad)
|
||||
band := pixel.Cycle([]pixel.Slot{pixel.Warn, pixel.Warn, light(k), pixel.Warn, pixel.Bad, pixel.Bad}, -step)
|
||||
for j := 0; 4*j < g.H; j++ {
|
||||
s, dir := band[j%len(band)], 1-2*(j%2)
|
||||
for i := range g.W/14 + 1 {
|
||||
x := wrap(rnd(i, 130+j)+dir*step, g.W+16) - 8
|
||||
g.Rect(x, 4*j+1+rnd(i, 140+j)%2, 4+rnd(i, 150+j)%8, 1, s)
|
||||
}
|
||||
}
|
||||
crust := pick(k, pixel.Edge, pixel.Shade)
|
||||
for i := range g.W * g.H / 300 {
|
||||
g.Rect(wrap(rnd(i, 160)+step/2, g.W), rnd(i, 161)%g.H, 2+rnd(i, 162)%3, 1, crust)
|
||||
}
|
||||
for i := range max(2, g.W/24) {
|
||||
t := step + rnd(i, 130)
|
||||
// Each life of a bubble rises somewhere new.
|
||||
x, y := rnd(t/12*64+i, 131)%g.W, 4+rnd(t/12*64+i, 132)%max(1, g.H-8)
|
||||
switch wrap(t, 12) {
|
||||
case 4:
|
||||
g.Dot(x, y, pixel.Warn)
|
||||
case 5:
|
||||
g.Disc(x, y, 1, pixel.Warn)
|
||||
case 6:
|
||||
g.Disc(x, y, 2, pixel.Warn)
|
||||
g.Dot(x-1, y-1, light(k))
|
||||
case 7:
|
||||
g.Circle(x, y, 3, pixel.Warn)
|
||||
g.Dot(x-1, y-1, light(k))
|
||||
case 8:
|
||||
for _, d := range [][2]int{{-4, 0}, {4, 0}, {0, -4}, {0, 4}} {
|
||||
g.Dot(x+d[0], y+d[1], pixel.Warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type LeavesOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// leafSlots color the leaves in turn.
|
||||
var leafSlots = []pixel.Slot{pixel.Warn, pixel.Bad, pixel.Accent}
|
||||
|
||||
// Leaves swing down side to side, turning on their stems.
|
||||
func Leaves(c *ui.Context, o LeavesOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Leaves", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { leaves(g, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
func leaves(g pixel.Canvas, step int) {
|
||||
for i := range max(2, g.W*g.H/500) {
|
||||
y := wrap(rnd(i, 60)+step, g.H+10) - 5
|
||||
x := wrap(rnd(i, 61)+step/4+sway(step+rnd(i, 62), 20, 5), g.W+10) - 5
|
||||
g.Sprite(x, y, leaf, (step+i)/3, leafSlots[i%len(leafSlots)], 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type LightningOptions struct {
|
||||
// Cue strikes each time it changes.
|
||||
Cue int
|
||||
// Width and Height are dots; zeros mean 96 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// strikeSteps run flash, half flash, bolt, bolt, dim bolt.
|
||||
const strikeSteps = 5
|
||||
|
||||
// Lightning flashes, then forks a bolt to the ground.
|
||||
func Lightning(c *ui.Context, o LightningOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Lightning", o.Width, o.Height, 96, 96)
|
||||
e := box(c, k, o.Label, w, h)
|
||||
step := k.Shot(c, e, o.Cue, 15, strikeSteps)
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if step == strikeSteps {
|
||||
return
|
||||
}
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { strike(g, k, step, o.Cue) })
|
||||
})
|
||||
}
|
||||
|
||||
// strike draws step of a strike; seed shapes the bolt.
|
||||
func strike(g pixel.Canvas, k pixel.Kit, step, seed int) {
|
||||
switch step {
|
||||
case 0:
|
||||
g.Rect(0, 0, g.W, g.H, light(k))
|
||||
return
|
||||
case 1:
|
||||
g.Veil(0, 0, g.W, g.H, light(k), 6)
|
||||
}
|
||||
core, glow := pick(k, pixel.Warn, pixel.Ink), pixel.Info
|
||||
if step == 4 {
|
||||
core = pixel.AccentLit
|
||||
}
|
||||
x, y := g.W/3+rnd(seed, 140)%max(1, g.W/3), 0
|
||||
fork := 1 + rnd(seed, 141)%3
|
||||
for i := 0; y < g.H; i++ {
|
||||
nx, ny := x+rnd(seed+i, 142)%7-3, y+4+rnd(seed+i, 143)%6
|
||||
g.Line(x+1, y, nx+1, ny, glow)
|
||||
g.Line(x, y, nx, ny, core)
|
||||
// One branch forks off and dies out.
|
||||
if i == fork {
|
||||
bx, by, dir := nx, ny, 1-2*(rnd(seed, 144)%2)
|
||||
for j := range 3 {
|
||||
ex, ey := bx+dir*(3+rnd(seed+j, 145)%3), by+3+rnd(seed+j, 146)%4
|
||||
g.Line(bx, by, ex, ey, core)
|
||||
bx, by = ex, ey
|
||||
}
|
||||
}
|
||||
x, y = nx, ny
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type MosaicOptions struct {
|
||||
// Cue pixelates the picture and back each time it changes.
|
||||
Cue int
|
||||
// Slot inks the sprite's # dots.
|
||||
Slot pixel.Slot
|
||||
Scale int
|
||||
Label string
|
||||
}
|
||||
|
||||
// blocks are block sides a step each: growing, then back.
|
||||
var blocks = [...]int{1, 2, 3, 4, 6, 8, 6, 4, 3, 2, 1}
|
||||
|
||||
// Mosaic pixelates sp in growing blocks, then sharpens it again.
|
||||
func Mosaic(c *ui.Context, sp *pixel.Sprite, o MosaicOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
sc := scale("Mosaic", o.Scale, 1)
|
||||
e := box(c, k, o.Label, sp.W*sc, sp.H*sc)
|
||||
b := blocks[k.Shot(c, e, o.Cue, 10, len(blocks)-1)]
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
if b == 1 {
|
||||
g.Sprite(0, 0, sp, 0, o.Slot, sc)
|
||||
return
|
||||
}
|
||||
for y := 0; y < sp.H; y += b {
|
||||
for x := 0; x < sp.W; x += b {
|
||||
if s, ok := majority(sp, o.Slot, x, y, b); ok {
|
||||
g.Rect(x*sc, y*sc, min(b, sp.W-x)*sc, min(b, sp.H-y)*sc, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// majority is a block's commonest slot; clear wins ties.
|
||||
func majority(sp *pixel.Sprite, ink pixel.Slot, x0, y0, b int) (pixel.Slot, bool) {
|
||||
var n [pixel.Slots + 1]int
|
||||
for y := y0; y < min(y0+b, sp.H); y++ {
|
||||
for x := x0; x < min(x0+b, sp.W); x++ {
|
||||
if s, ok := sp.At(0, x, y, ink); ok {
|
||||
n[s]++
|
||||
} else {
|
||||
n[pixel.Slots]++
|
||||
}
|
||||
}
|
||||
}
|
||||
best := pixel.Slots
|
||||
for s := range pixel.Slots {
|
||||
if n[s] > n[best] {
|
||||
best = s
|
||||
}
|
||||
}
|
||||
return pixel.Slot(best), best != pixel.Slots
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type MountainsOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Mountains scroll three ridges: far ones slow, near ones quick.
|
||||
func Mountains(c *ui.Context, o MountainsOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Mountains", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { mountains(g, k, g.Step(8), 3) })
|
||||
})
|
||||
}
|
||||
|
||||
// tri rises from 0 to a, then falls, over p.
|
||||
func tri(t, p, a int) int {
|
||||
t, h := wrap(t, p), p/2
|
||||
if t < h {
|
||||
return a * t / h
|
||||
}
|
||||
return a * (p - t) / h
|
||||
}
|
||||
|
||||
// mountains draws n ridges; ridge l moves every 8>>l steps.
|
||||
func mountains(g pixel.Canvas, k pixel.Kit, step, n int) {
|
||||
slots := [3]pixel.Slot{pick(k, pixel.Shade, pixel.Face), pick(k, pixel.Faint, pixel.Back), pick(k, pixel.Edge, pixel.Well)}
|
||||
snowy := k.Palette().Season == pixel.Winter
|
||||
for l := range n {
|
||||
shift := step / (8 >> l)
|
||||
for x := range g.W {
|
||||
u := x + shift
|
||||
h := g.H*(7-2*l)/16 + tri(u+rnd(l, 100), 48+16*l, g.H/5) + tri(2*u+rnd(l, 101), 37, g.H/12) + rnd(u, 102+l)%2
|
||||
g.VLine(x, g.H-h, h, slots[l])
|
||||
// Winter caps the far peaks with snow.
|
||||
if snowy && l == 0 && h > g.H*9/16 {
|
||||
g.VLine(x, g.H-h, 2, light(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type OceanOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 64.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Ocean rolls wave crests toward you by cycling their slots.
|
||||
func Ocean(c *ui.Context, o OceanOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Ocean", o.Width, o.Height, 160, 64)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { ocean(g, k, g.Step(6)) })
|
||||
})
|
||||
}
|
||||
|
||||
// ocean's crest rows grow sparser and longer downward.
|
||||
func ocean(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
g.Rect(0, 0, g.W, g.H, pixel.Info)
|
||||
g.Dither(0, 0, g.W, 2, pixel.Info, light(k), 6)
|
||||
// Crest row j shows the band turned back each step.
|
||||
band := pixel.Cycle([]pixel.Slot{pixel.Info, pixel.Info, pixel.AccentLit, light(k)}, -step)
|
||||
for y, j := 3, 0; y < g.H; j++ {
|
||||
if s := band[j%len(band)]; s != pixel.Info {
|
||||
for i := range max(1, g.W/12) {
|
||||
g.HLine(rnd(i, 120+j)%g.W, y, 2+j/2, s)
|
||||
}
|
||||
}
|
||||
y += 2 + j/2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type PaletteCycleOptions struct {
|
||||
// FPS is steps a second; zero means 8.
|
||||
FPS int
|
||||
// Slot inks the sprite's # dots.
|
||||
Slot pixel.Slot
|
||||
Scale int
|
||||
Label string
|
||||
}
|
||||
|
||||
// PaletteCycle draws sp, rotating band's slots one place a step.
|
||||
func PaletteCycle(c *ui.Context, sp *pixel.Sprite, band []pixel.Slot, o PaletteCycleOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
fps, sc := pixel.Or("PaletteCycle FPS", o.FPS, 8), scale("PaletteCycle", o.Scale, 1)
|
||||
if len(band) < 2 {
|
||||
panic(fmt.Sprintf("fx: PaletteCycle band %v needs two slots", band))
|
||||
}
|
||||
return box(c, k, o.Label, sp.W*sc, sp.H*sc).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Cycled(band, g.Step(fps)).Sprite(0, 0, sp, 0, o.Slot, sc)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type PetalsOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Petals fall on a breeze, turning as they go.
|
||||
func Petals(c *ui.Context, o PetalsOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Petals", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { petals(g, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
// petals fall a row a step, drifting left every three.
|
||||
func petals(g pixel.Canvas, step int) {
|
||||
for i := range g.W * g.H / 260 {
|
||||
y := wrap(rnd(i, 50)+step, g.H+6) - 3
|
||||
x := wrap(rnd(i, 51)-step/3+sway(step+rnd(i, 52), 24, 3), g.W+4) - 2
|
||||
g.Sprite(x, y, petal, step/2+i, pixel.Ink, 1)
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type RainOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Rain slants streaks down; each splashes as it lands.
|
||||
func Rain(c *ui.Context, o RainOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Rain", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { rain(g, k, g.Step(12)) })
|
||||
})
|
||||
}
|
||||
|
||||
// rain drops fall three rows, a dot left, each step.
|
||||
func rain(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
s := pick(k, pixel.Faint, pixel.Info)
|
||||
fall := (g.H+4)/3 + 1
|
||||
for i := range g.W * g.H / 160 {
|
||||
p := wrap(step+rnd(i, 30), fall+3)
|
||||
x := wrap(rnd(i, 31)-p, g.W)
|
||||
switch {
|
||||
case p < fall:
|
||||
y := 3*p - 4
|
||||
g.Line(x+1, y, x, y+3, s)
|
||||
case p == fall:
|
||||
g.Dot(x-1, g.H-2, s)
|
||||
g.Dot(x+1, g.H-2, s)
|
||||
case p == fall+1:
|
||||
g.Dot(x-2, g.H-3, s)
|
||||
g.Dot(x+2, g.H-3, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type ScanlinesOptions struct {
|
||||
// Slot shades the rows; zero means Shade.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// Scanlines shades every other row of content at half strength.
|
||||
func Scanlines(c *ui.Context, o ScanlinesOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, pixel.Shade)
|
||||
return cover(k, ui.Column(c), func(g pixel.Canvas) {
|
||||
for y := 1; y < g.H; y += 2 {
|
||||
g.Checker(0, y, g.W, 1, s)
|
||||
}
|
||||
}, content)
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
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 TestScanlinesShadeHalfOfEveryOtherRow(t *testing.T) {
|
||||
d := dots(view(40, 20, pixel.SpringDay, newClock(), false, func(c *ui.Context) {
|
||||
fx.Scanlines(c, fx.ScanlinesOptions{}, func() { solid(c, 40, 20, pixel.Info) })
|
||||
}).Image(), pixel.SpringDay, 40, 20)
|
||||
for y := range 20 {
|
||||
if want := 20 * (y % 2); count(d, 0, y, 40, 1, pixel.Shade) != want {
|
||||
t.Fatalf("row %d holds %d Shade dots, want %d", y, count(d, 0, y, 40, 1, pixel.Shade), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVignetteDarkensTowardTheEdges(t *testing.T) {
|
||||
d := dots(view(80, 48, pixel.SpringDay, newClock(), false, func(c *ui.Context) {
|
||||
fx.Vignette(c, fx.VignetteOptions{}, func() { solid(c, 80, 48, pixel.Lit) })
|
||||
}).Image(), pixel.SpringDay, 80, 48)
|
||||
if d[0][0] != int(pixel.Ink) || d[47][79] != int(pixel.Ink) || count(d, 24, 14, 32, 20, pixel.Ink) != 0 {
|
||||
t.Fatal("corners must be solid and the middle clear")
|
||||
}
|
||||
if edge, in := count(d, 0, 0, 1, 48, pixel.Ink), count(d, 12, 0, 1, 48, pixel.Ink); edge <= in || in == 0 {
|
||||
t.Fatalf("the edge column holds %d dark dots, one further in %d", edge, in)
|
||||
}
|
||||
}
|
||||
|
||||
// cued builds an effect whose cue the test changes.
|
||||
func cued(clk *clock, reduce bool, w, h int, build func(c *ui.Context, cue int)) (*ui.Tester, *int) {
|
||||
cue := new(int)
|
||||
return view(w, h, pixel.SpringDay, clk, reduce, func(c *ui.Context) { build(c, *cue) }), cue
|
||||
}
|
||||
|
||||
func TestScreenShakeJoltsThenSettles(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, reduce, 40, 30, func(c *ui.Context, cue int) {
|
||||
fx.ScreenShake(c, fx.ScreenShakeOptions{Cue: cue}, func() { solid(c, 20, 10, pixel.Info) })
|
||||
})
|
||||
at := func() (float32, float32) {
|
||||
r, ok := tt.Find("content")
|
||||
if !ok {
|
||||
t.Fatal("no content")
|
||||
}
|
||||
return r.X, r.Y
|
||||
}
|
||||
x0, y0 := at()
|
||||
*cue++
|
||||
tt.Frame()
|
||||
// The first jolt is three dots right and two up.
|
||||
if x, y := at(); !reduce && (x != x0+6 || y != y0-4) || reduce && (x != x0 || y != y0) {
|
||||
t.Fatalf("reduce %v: content moved from %v,%v to %v,%v", reduce, x0, y0, x, y)
|
||||
}
|
||||
clk.t = clk.t.Add(time.Second)
|
||||
tt.Frame()
|
||||
if x, y := at(); x != x0 || y != y0 {
|
||||
t.Fatalf("reduce %v: the shake did not settle", reduce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlashFillsAStepThenHalfAStep(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, reduce, 40, 20, func(c *ui.Context, cue int) {
|
||||
fx.Flash(c, fx.FlashOptions{Cue: cue}, func() { solid(c, 40, 20, pixel.Info) })
|
||||
})
|
||||
lit := func() int { return count(dots(tt.Image(), pixel.SpringDay, 40, 20), 0, 0, 40, 20, pixel.Lit) }
|
||||
if lit() != 0 {
|
||||
t.Fatal("flashed before its cue")
|
||||
}
|
||||
*cue++
|
||||
want := []int{800, 400, 0}
|
||||
if reduce {
|
||||
want = []int{0, 0, 0}
|
||||
}
|
||||
for _, w := range want {
|
||||
tt.Frame()
|
||||
if lit() != w {
|
||||
t.Fatalf("reduce %v: %d Lit dots, want %d", reduce, lit(), w)
|
||||
}
|
||||
clk.t = clk.t.Add(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFadeDitherStepsThroughBayerLevels(t *testing.T) {
|
||||
clk, covered := newClock(), true
|
||||
tt := view(32, 16, pixel.SpringDay, clk, false, func(c *ui.Context) {
|
||||
fx.FadeDither(c, covered, fx.FadeDitherOptions{}, func() { solid(c, 32, 16, pixel.Lit) })
|
||||
})
|
||||
dark := func() int { return count(dots(tt.Image(), pixel.SpringDay, 32, 16), 0, 0, 32, 16, pixel.Ink) }
|
||||
// It starts covered; uncovering drops two levels a step.
|
||||
covered = false
|
||||
for level := 16; level >= 0; level -= 2 {
|
||||
tt.Frame()
|
||||
if dark() != 32*level {
|
||||
t.Fatalf("level %d: %d dark dots", level, dark())
|
||||
}
|
||||
clk.t = clk.t.Add(tick(12))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIrisWipeClosesOnTheMiddle(t *testing.T) {
|
||||
clk, closed := newClock(), false
|
||||
tt := view(64, 32, pixel.SpringDay, clk, false, func(c *ui.Context) {
|
||||
fx.IrisWipe(c, closed, fx.IrisWipeOptions{}, func() { solid(c, 64, 32, pixel.Lit) })
|
||||
})
|
||||
closed = true
|
||||
last := -1
|
||||
for step := range 9 {
|
||||
tt.Frame()
|
||||
d := dots(tt.Image(), pixel.SpringDay, 64, 32)
|
||||
n := count(d, 0, 0, 64, 32, pixel.Ink)
|
||||
switch {
|
||||
case n <= last && step < 8:
|
||||
t.Fatalf("step %d: the iris did not close further", step)
|
||||
case count(d, 0, 0, 32, 32, pixel.Ink) != count(d, 32, 0, 32, 32, pixel.Ink):
|
||||
t.Fatalf("step %d: the iris is off the middle", step)
|
||||
case step < 8 && d[16][32] != int(pixel.Lit), step == 8 && n != 64*32:
|
||||
t.Fatalf("step %d: the middle shows %d", step, d[16][32])
|
||||
}
|
||||
last = n
|
||||
clk.t = clk.t.Add(tick(12))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDissolveCoversDotsInShuffledOrder(t *testing.T) {
|
||||
clk, covered := newClock(), false
|
||||
tt := view(64, 32, pixel.SpringDay, clk, false, func(c *ui.Context) {
|
||||
fx.Dissolve(c, covered, fx.DissolveOptions{}, func() { solid(c, 64, 32, pixel.Lit) })
|
||||
})
|
||||
covered = true
|
||||
var prev [][]int
|
||||
for step := range 9 {
|
||||
tt.Frame()
|
||||
d := dots(tt.Image(), pixel.SpringDay, 64, 32)
|
||||
if n := count(d, 0, 0, 64, 32, pixel.Ink); n < 2048*step/8-120 || n > 2048*step/8+120 {
|
||||
t.Fatalf("step %d covered %d of 2048", step, n)
|
||||
}
|
||||
for y := range d {
|
||||
for x := range d[y] {
|
||||
if prev != nil && prev[y][x] == int(pixel.Ink) && d[y][x] != int(pixel.Ink) {
|
||||
t.Fatalf("step %d uncovered %d,%d", step, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Halfway, every row and column is part covered.
|
||||
if step == 4 {
|
||||
for y := range 32 {
|
||||
if n := count(d, 0, y, 64, 1, pixel.Ink); n == 0 || n == 64 {
|
||||
t.Fatalf("row %d has %d of 64 covered", y, n)
|
||||
}
|
||||
}
|
||||
for x := range 64 {
|
||||
if n := count(d, x, 0, 1, 32, pixel.Ink); n == 0 || n == 32 {
|
||||
t.Fatalf("column %d has %d of 32 covered", x, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = d
|
||||
clk.t = clk.t.Add(tick(12))
|
||||
}
|
||||
}
|
||||
|
||||
// quilt is 16×16 dots of four slots; neighbors differ.
|
||||
var quilt = pixel.SpriteOf(16, 16, func(x, y int) (pixel.Slot, bool) {
|
||||
return []pixel.Slot{pixel.Good, pixel.Info, pixel.Bad, pixel.Warn}[(x+y*3)%4], true
|
||||
})
|
||||
|
||||
func TestMosaicGrowsBlocksThenSharpens(t *testing.T) {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, false, 16, 16, func(c *ui.Context, cue int) { fx.Mosaic(c, quilt, fx.MosaicOptions{Cue: cue}) })
|
||||
plain := dots(tt.Image(), pixel.SpringDay, 16, 16)
|
||||
*cue++
|
||||
for step, b := range []int{1, 2, 3, 4, 6, 8, 6, 4, 3, 2, 1} {
|
||||
tt.Frame()
|
||||
d := dots(tt.Image(), pixel.SpringDay, 16, 16)
|
||||
for y := range 16 {
|
||||
for x := range 16 {
|
||||
if d[y][x] != d[y/b*b][x/b*b] {
|
||||
t.Fatalf("step %d: block %d is not one slot at %d,%d", step, b, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
if b == 1 != same(d, plain) {
|
||||
t.Fatalf("step %d: blocks of %d, picture changed %v", step, b, !same(d, plain))
|
||||
}
|
||||
clk.t = clk.t.Add(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMosaicBlocksTakeTheirCommonestSlot(t *testing.T) {
|
||||
// One Good dot in a field of Bad.
|
||||
sp := pixel.SpriteOf(8, 8, func(x, y int) (pixel.Slot, bool) {
|
||||
if x == 0 && y == 0 {
|
||||
return pixel.Good, true
|
||||
}
|
||||
return pixel.Bad, true
|
||||
})
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, false, 8, 8, func(c *ui.Context, cue int) { fx.Mosaic(c, sp, fx.MosaicOptions{Cue: cue}) })
|
||||
*cue++
|
||||
tt.Frame()
|
||||
clk.t = clk.t.Add(500*time.Millisecond + time.Microsecond)
|
||||
tt.Frame()
|
||||
if d := dots(tt.Image(), pixel.SpringDay, 8, 8); count(d, 0, 0, 8, 8, pixel.Bad) != 64 {
|
||||
t.Fatal("an 8-dot block must take its commonest slot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlitchScramblesAFewFramesThenSettles(t *testing.T) {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, reduce, 22, 16, func(c *ui.Context, cue int) { fx.Glitch(c, quilt, fx.GlitchOptions{Cue: cue}) })
|
||||
plain := dots(tt.Image(), pixel.SpringDay, 22, 16)
|
||||
*cue++
|
||||
changed := false
|
||||
for range 6 {
|
||||
tt.Frame()
|
||||
changed = changed || !same(plain, dots(tt.Image(), pixel.SpringDay, 22, 16))
|
||||
clk.t = clk.t.Add(tick(15))
|
||||
}
|
||||
tt.Frame()
|
||||
if changed == reduce || !same(plain, dots(tt.Image(), pixel.SpringDay, 22, 16)) {
|
||||
t.Fatalf("reduce %v: glitched %v, then not back to the sprite", reduce, changed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCuesSpentUnderReduceMotionNeverReplay(t *testing.T) {
|
||||
for name, build := range map[string]func(c *ui.Context, cue int){
|
||||
"Lightning": func(c *ui.Context, cue int) { fx.Lightning(c, fx.LightningOptions{Cue: cue, Width: 32, Height: 32}) },
|
||||
"Explosion": func(c *ui.Context, cue int) { fx.Explosion(c, fx.ExplosionOptions{Cue: cue, Size: 32}) },
|
||||
"Mosaic": func(c *ui.Context, cue int) { fx.Mosaic(c, quilt, fx.MosaicOptions{Cue: cue}) },
|
||||
"Glitch": func(c *ui.Context, cue int) { fx.Glitch(c, quilt, fx.GlitchOptions{Cue: cue}) },
|
||||
"Flash": func(c *ui.Context, cue int) {
|
||||
fx.Flash(c, fx.FlashOptions{Cue: cue}, func() { solid(c, 32, 16, pixel.Info) })
|
||||
},
|
||||
"ScreenShake": func(c *ui.Context, cue int) {
|
||||
fx.ScreenShake(c, fx.ScreenShakeOptions{Cue: cue}, func() { solid(c, 20, 10, pixel.Info) })
|
||||
},
|
||||
} {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, false, 40, 40, build)
|
||||
before := dots(tt.Image(), pixel.SpringDay, 40, 40)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
*cue++
|
||||
tt.Frame()
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: false, TextScale: 1})
|
||||
for _, d := range []time.Duration{0, 300 * time.Millisecond} {
|
||||
clk.t = clk.t.Add(d)
|
||||
tt.Frame()
|
||||
if !same(before, dots(tt.Image(), pixel.SpringDay, 40, 40)) {
|
||||
t.Errorf("%s replayed a cue spent under Reduce Motion", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransitionsKeepTheirEndAfterReduceMotion(t *testing.T) {
|
||||
clk, covered := newClock(), false
|
||||
tt := view(32, 16, pixel.SpringDay, clk, false, func(c *ui.Context) {
|
||||
fx.FadeDither(c, covered, fx.FadeDitherOptions{}, func() { solid(c, 32, 16, pixel.Lit) })
|
||||
})
|
||||
covered = true
|
||||
tt.Frame()
|
||||
clk.t = clk.t.Add(tick(12))
|
||||
tt.Frame()
|
||||
for _, reduce := range []bool{true, false} {
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1})
|
||||
tt.Frame()
|
||||
if n := count(dots(tt.Image(), pixel.SpringDay, 32, 16), 0, 0, 32, 16, pixel.Ink); n != 512 {
|
||||
t.Fatalf("reduce %v: %d of 512 covered", reduce, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// blank is whether a box shows only the page.
|
||||
func blank(tt *ui.Tester, w, h int) bool {
|
||||
return count(dots(tt.Image(), pixel.SpringDay, w, h), 0, 0, w, h, pixel.Back) == w*h
|
||||
}
|
||||
|
||||
func TestOneShotsRunOnlyOnTheirCue(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
fps int
|
||||
steps int
|
||||
build func(c *ui.Context, cue int)
|
||||
}{
|
||||
"Explosion": {12, 8, func(c *ui.Context, cue int) { fx.Explosion(c, fx.ExplosionOptions{Cue: cue}) }},
|
||||
"Lightning": {15, 5, func(c *ui.Context, cue int) { fx.Lightning(c, fx.LightningOptions{Cue: cue, Width: 48, Height: 48}) }},
|
||||
} {
|
||||
for _, reduce := range []bool{false, true} {
|
||||
clk := newClock()
|
||||
tt, cue := cued(clk, reduce, 48, 48, tc.build)
|
||||
if !blank(tt, 48, 48) {
|
||||
t.Fatalf("%s showed before its cue", name)
|
||||
}
|
||||
*cue++
|
||||
for step := range tc.steps {
|
||||
tt.Frame()
|
||||
if blank(tt, 48, 48) != reduce {
|
||||
t.Fatalf("%s, reduce %v: step %d blank %v", name, reduce, step, !reduce)
|
||||
}
|
||||
clk.t = clk.t.Add(tick(tc.fps))
|
||||
}
|
||||
tt.Frame()
|
||||
if !blank(tt, 48, 48) {
|
||||
t.Fatalf("%s outlived its steps", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightningFlashesThenForks(t *testing.T) {
|
||||
bolt := func(cue int) [][]int {
|
||||
clk := newClock()
|
||||
tt, c := cued(clk, false, 48, 48, func(c *ui.Context, cue int) {
|
||||
fx.Lightning(c, fx.LightningOptions{Cue: cue, Width: 48, Height: 48})
|
||||
})
|
||||
*c = cue
|
||||
tt.Frame()
|
||||
if d := dots(tt.Image(), pixel.SpringDay, 48, 48); count(d, 0, 0, 48, 48, pixel.Lit) != 48*48 {
|
||||
t.Fatal("a strike opens with a full flash")
|
||||
}
|
||||
clk.t = clk.t.Add(2 * tick(15))
|
||||
tt.Frame()
|
||||
return dots(tt.Image(), pixel.SpringDay, 48, 48)
|
||||
}
|
||||
a, b := bolt(1), bolt(2)
|
||||
if count(a, 0, 0, 48, 48, pixel.Warn) == 0 || count(a, 0, 47, 48, 1, pixel.Warn) == 0 {
|
||||
t.Fatal("the bolt must reach the ground")
|
||||
}
|
||||
if same(a, b) {
|
||||
t.Fatal("two strikes drew the same bolt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSparkleCirclesItsMiddle(t *testing.T) {
|
||||
clk := newClock()
|
||||
tt := view(40, 40, pixel.SpringDay, clk, false, func(c *ui.Context) { fx.Sparkle(c, fx.SparkleOptions{}) })
|
||||
var quarters [4]int
|
||||
for range 8 {
|
||||
d := dots(tt.Image(), pixel.SpringDay, 40, 40)
|
||||
if count(d, 0, 0, 24, 24, pixel.Warn) != count(d, 0, 0, 40, 40, pixel.Warn) {
|
||||
t.Fatal("a star left the square")
|
||||
}
|
||||
for i, q := range [][2]int{{0, 0}, {12, 0}, {0, 12}, {12, 12}} {
|
||||
quarters[i] += count(d, q[0], q[1], 12, 12, pixel.Warn)
|
||||
}
|
||||
clk.t = clk.t.Add(tick(10))
|
||||
tt.Frame()
|
||||
}
|
||||
for i, n := range quarters {
|
||||
if n == 0 {
|
||||
t.Fatalf("no star twinkled in quarter %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type ScreenShakeOptions struct {
|
||||
// Cue shakes the content each time it changes.
|
||||
Cue int
|
||||
// Amount is the widest jolt in dots; zero means 3.
|
||||
Amount int
|
||||
}
|
||||
|
||||
// quakes are the jolts, a step each, in Amount thirds.
|
||||
var quakes = [...][2]int{{3, -2}, {-3, 2}, {2, 3}, {-3, -2}, {2, 1}, {-1, -2}, {1, 1}, {-1, 0}, {0, 0}}
|
||||
|
||||
// ScreenShake jolts its content both ways, settling as it goes.
|
||||
func ScreenShake(c *ui.Context, o ScreenShakeOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
a := pixel.Or("ScreenShake Amount", o.Amount, 3)
|
||||
e := ui.Box(c)
|
||||
q := quakes[k.Shot(c, e, o.Cue, 15, len(quakes)-1)]
|
||||
dx, dy := q[0]*a/3, q[1]*a/3
|
||||
return e.Padding(k.D(a+dy), k.D(a-dx), k.D(a-dy), k.D(a+dx)).Children(content)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type SeasonSceneOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// SeasonScene paints a season at an hour, in its palette.
|
||||
func SeasonScene(c *ui.Context, season pixel.Season, hour int, o SeasonSceneOptions) *ui.Element {
|
||||
checkHour("SeasonScene", hour)
|
||||
if season > pixel.Winter {
|
||||
panic(fmt.Sprintf("fx: SeasonScene season %d", season))
|
||||
}
|
||||
s := pixel.Current(c)
|
||||
s.Palette = pixel.Find(season, hour < 6 || hour >= 18)
|
||||
var e *ui.Element
|
||||
pixel.Scope(c, s, func() {
|
||||
k := pixel.K(c)
|
||||
w, h := size("SeasonScene", o.Width, o.Height, 160, 96)
|
||||
e = box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { scene(g, k, season, hour, g.Step(8)) })
|
||||
})
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// scene layers sky, sun or moon, ridges, trees and weather.
|
||||
func scene(g pixel.Canvas, k pixel.Kit, season pixel.Season, hour, step int) {
|
||||
night := k.Palette().Night
|
||||
sky(g, k, step/2)
|
||||
sunMoon(g, k, hour, step/2)
|
||||
if !night {
|
||||
clouds(g, step)
|
||||
}
|
||||
mountains(g, k, step, 2)
|
||||
top := g.H - g.H/6
|
||||
g.Rect(0, top, g.W, g.H-top, [4]pixel.Slot{pixel.Good, pixel.Good, pixel.Warn, light(k)}[season])
|
||||
if night {
|
||||
g.Veil(0, top, g.W, g.H-top, dark(k), 8)
|
||||
}
|
||||
tree := trees[season]
|
||||
for x := 4 + rnd(int(season), 190)%12; x < g.W-tree.W; x += 30 + rnd(x, 191)%30 {
|
||||
y := top - tree.H + 3
|
||||
if !night {
|
||||
g.Sprite(x, y, tree, 0, pixel.Ink, 1)
|
||||
continue
|
||||
}
|
||||
// By night trees stand dark; only snow catches the light.
|
||||
g.Silhouette(x, y, tree, 0, dark(k), 1)
|
||||
for ty := range tree.H {
|
||||
for tx := range tree.W {
|
||||
if s, ok := tree.At(0, tx, ty, pixel.Ink); ok && s == pixel.Lit {
|
||||
g.Dot(x+tx, y+ty, light(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
switch season {
|
||||
case pixel.Spring:
|
||||
petals(g, step)
|
||||
case pixel.Summer:
|
||||
if night {
|
||||
fireflies(g, step)
|
||||
}
|
||||
case pixel.Autumn:
|
||||
leaves(g, step)
|
||||
case pixel.Winter:
|
||||
snow(g, k, step)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type SkyOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Sky dithers down the palette's sky slots; night shows stars.
|
||||
func Sky(c *ui.Context, o SkyOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Sky", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { sky(g, k, g.Step(4)) })
|
||||
})
|
||||
}
|
||||
|
||||
// sky fills g in bands cut at height percents.
|
||||
func sky(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
bands, cuts := []pixel.Slot{pixel.Info, pixel.AccentLit, pixel.Lit}, []int{0, 55, 85, 100}
|
||||
if k.Palette().Night {
|
||||
bands, cuts = []pixel.Slot{pixel.Well, pixel.Back, pixel.Face, pixel.Lit}, []int{0, 35, 60, 82, 100}
|
||||
}
|
||||
for i, s := range bands {
|
||||
y0, y1 := g.H*cuts[i]/100, g.H*cuts[i+1]/100
|
||||
g.Rect(0, y0, g.W, y1-y0, s)
|
||||
}
|
||||
// Seams dither five rows from one band into the next.
|
||||
for i := 1; i < len(bands); i++ {
|
||||
y := g.H * cuts[i] / 100
|
||||
for j := range 5 {
|
||||
g.Dither(0, y-2+j, g.W, 1, bands[i-1], bands[i], 2+3*j)
|
||||
}
|
||||
}
|
||||
if k.Palette().Night {
|
||||
stars(g, k, step)
|
||||
}
|
||||
}
|
||||
|
||||
// stars twinkle over the top two thirds.
|
||||
func stars(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
tones := []pixel.Slot{light(k), light(k), pixel.AccentLit, light(k), pixel.Warn}
|
||||
for i := range g.W * g.H / 90 {
|
||||
x, y := rnd(i, 1)%g.W, rnd(i, 2)%max(1, g.H*2/3)
|
||||
s, t := tones[i%len(tones)], wrap(step+rnd(i, 3), 8)
|
||||
switch {
|
||||
case t == 0:
|
||||
g.Dot(x, y, pixel.Faint)
|
||||
case i%9 == 0 && t < 3:
|
||||
g.Rect(x-1, y, 3, 1, s)
|
||||
g.Rect(x, y-1, 1, 3, s)
|
||||
default:
|
||||
g.Dot(x, y, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type SnowOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Snow drifts flakes down, swaying; near ones are big, quick.
|
||||
func Snow(c *ui.Context, o SnowOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Snow", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { snow(g, k, g.Step(8)) })
|
||||
})
|
||||
}
|
||||
|
||||
func snow(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
s := light(k)
|
||||
for i := range g.W * g.H / 110 {
|
||||
near := rnd(i, 40)%3 == 0
|
||||
speed := 1
|
||||
if near {
|
||||
speed = 2
|
||||
}
|
||||
y := wrap(rnd(i, 41)+step*speed, g.H+4) - 2
|
||||
x := wrap(rnd(i, 42)+sway(step+rnd(i, 43), 16, 2), g.W)
|
||||
if near {
|
||||
g.Rect(x, y, 2, 2, s)
|
||||
} else {
|
||||
g.Dot(x, y, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type SparkleOptions struct {
|
||||
// Size is the square's side in dots; zero means 24.
|
||||
Size int
|
||||
// Slot inks the stars; zero means Warn.
|
||||
Slot pixel.Slot
|
||||
Label string
|
||||
}
|
||||
|
||||
// glints are a star's frames by step; -1 is dark.
|
||||
var glints = [...]int{0, 1, 2, 1, 0, -1, -1, -1}
|
||||
|
||||
// Sparkle twinkles four stars around its middle, in turn.
|
||||
func Sparkle(c *ui.Context, o SparkleOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("Sparkle Size", o.Size, 24)
|
||||
s := slotOr(o.Slot, pixel.Warn)
|
||||
return box(c, k, o.Label, n, n).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { sparkle(g, s, g.Step(10)) })
|
||||
})
|
||||
}
|
||||
|
||||
// sparkle moves each star to a new spot every cycle.
|
||||
func sparkle(g pixel.Canvas, s pixel.Slot, step int) {
|
||||
for i := range 4 {
|
||||
t := step + 2*i
|
||||
f := glints[wrap(t, len(glints))]
|
||||
if f < 0 {
|
||||
continue
|
||||
}
|
||||
seed := t/len(glints)*4 + i
|
||||
a := float64(i)*math.Pi/2 + float64(rnd(seed, 170)%60-30)*math.Pi/180
|
||||
d := float64(g.W) * float64(20+rnd(seed, 171)%20) / 100
|
||||
x := g.W/2 + int(math.Round(d*math.Cos(a))) - 2
|
||||
y := g.H/2 + int(math.Round(d*math.Sin(a))) - 2
|
||||
g.Sprite(x, y, twinkle, f, s, 1)
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package fx
|
||||
|
||||
import "git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
|
||||
var smallCloud = pixel.MustSprite(`
|
||||
.....lll......
|
||||
..llllllll.l..
|
||||
.llllllllllll.
|
||||
llllllllllllll
|
||||
fllllllllllllf
|
||||
.ffffffffffff.
|
||||
`)
|
||||
|
||||
var bigCloud = pixel.MustSprite(`
|
||||
.........llll...........
|
||||
....llll.llllll.........
|
||||
..lllllllllllllll.llll..
|
||||
.lllllllllllllllllllllll
|
||||
llllllllllllllllllllllll
|
||||
lllllllllllllllllllllllf
|
||||
fllllllllllllllllllllllf
|
||||
.fffllllllllllllllllfff.
|
||||
...ffffffffffffffffff...
|
||||
`)
|
||||
|
||||
// trees stand in the scene, one per season.
|
||||
var trees = [4]*pixel.Sprite{
|
||||
pixel.MustSprite(`
|
||||
.....AAAAA.....
|
||||
...AAAaAAAAA...
|
||||
..AAAAAAAAaAA..
|
||||
.AAaAAAAAAAAAA.
|
||||
.AAAAAAaAAAAAA.
|
||||
AAAAAAAAAAAaAAA
|
||||
AAaAAAAAAAAAAAA
|
||||
AAAAAAaAAAAAAAA
|
||||
.AAAAAAAAAaAAA.
|
||||
..AAAaAAAAAAA..
|
||||
...AAAAnAAAA...
|
||||
.....AnnnA.....
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
.....nnnnn.....
|
||||
`),
|
||||
pixel.MustSprite(`
|
||||
.....ggggg.....
|
||||
...ggglgggggg..
|
||||
..ggllggggggg..
|
||||
.gggllgggggggg.
|
||||
.ggggggggggggg.
|
||||
ggggggggggggggg
|
||||
ggggggggggggggg
|
||||
.ggggggggggggg.
|
||||
.ggggggggggggg.
|
||||
..ggggggggggg..
|
||||
...gggnggggg...
|
||||
.....ggnngg....
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
.....nnnnn.....
|
||||
`),
|
||||
pixel.MustSprite(`
|
||||
.....yyyyy.....
|
||||
...yyryyyyay...
|
||||
..yyyyyyryyyy..
|
||||
.yyayyyyyyyyry.
|
||||
.yyyyyryyyyyyy.
|
||||
yyyryyyyyyayyyy
|
||||
yyyyyyyyryyyyyy
|
||||
.yyyayyyyyyyry.
|
||||
.yyyyyyyyyyyyy.
|
||||
..yyyryyyyyyy..
|
||||
...yyyynyyyy...
|
||||
.....yynnyy....
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
.....nnnnn.....
|
||||
`),
|
||||
pixel.MustSprite(`
|
||||
.......l.......
|
||||
......lgl......
|
||||
.....lgggl.....
|
||||
......ggg......
|
||||
....llgggll....
|
||||
...lgggggggl...
|
||||
....ggggggg....
|
||||
...llgggggll...
|
||||
..lgggggggggl..
|
||||
...ggggggggg...
|
||||
..llgggggggll..
|
||||
.lgggggggggggl.
|
||||
lgggggggggggggl
|
||||
......nnn......
|
||||
......nnn......
|
||||
......nnn......
|
||||
.....nnnnn.....
|
||||
`),
|
||||
}
|
||||
|
||||
// petal turns: face, tilt, edge, tilt back.
|
||||
var petal = pixel.MustSprite(`
|
||||
.A.
|
||||
AAA
|
||||
.a.
|
||||
|
||||
..A
|
||||
.Aa
|
||||
a..
|
||||
|
||||
...
|
||||
AAa
|
||||
...
|
||||
|
||||
A..
|
||||
aA.
|
||||
..a
|
||||
`)
|
||||
|
||||
// leaf turns about its stem; # takes the leaf's slot.
|
||||
var leaf = pixel.MustSprite(`
|
||||
.###.
|
||||
#####
|
||||
#####
|
||||
.###.
|
||||
..#..
|
||||
|
||||
..##.
|
||||
.###.
|
||||
####.
|
||||
.##..
|
||||
..#..
|
||||
|
||||
..#..
|
||||
..#..
|
||||
..#..
|
||||
..#..
|
||||
..#..
|
||||
|
||||
.##..
|
||||
.###.
|
||||
.####
|
||||
..##.
|
||||
..#..
|
||||
`)
|
||||
|
||||
// twinkle grows from a dot to a star.
|
||||
var twinkle = pixel.MustSprite(`
|
||||
.....
|
||||
.....
|
||||
..#..
|
||||
.....
|
||||
.....
|
||||
|
||||
.....
|
||||
..#..
|
||||
.###.
|
||||
..#..
|
||||
.....
|
||||
|
||||
..#..
|
||||
..#..
|
||||
#####
|
||||
..#..
|
||||
..#..
|
||||
`)
|
||||
|
||||
// nightLit draws Lit art in Ink, bright at night.
|
||||
func nightLit(g pixel.Canvas, k pixel.Kit) pixel.Canvas {
|
||||
if k.Palette().Night {
|
||||
return g.Cycled([]pixel.Slot{pixel.Lit, pixel.Ink}, 1)
|
||||
}
|
||||
return g
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type StarfieldOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Starfield scrolls three layers of stars, near ones fastest.
|
||||
func Starfield(c *ui.Context, o StarfieldOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Starfield", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { starfield(g, k, g.Step(12)) })
|
||||
})
|
||||
}
|
||||
|
||||
// starfield moves layer l one dot every 4>>l steps.
|
||||
func starfield(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
g.Rect(0, 0, g.W, g.H, dark(k))
|
||||
for l, s := range []pixel.Slot{pixel.Faint, pixel.AccentLit, light(k)} {
|
||||
shift := step / (4 >> l)
|
||||
for i := range g.W * g.H / (120 + 80*l) {
|
||||
x, y := wrap(rnd(i, 20+l)-shift, g.W), rnd(i, 23+l)%g.H
|
||||
switch t := wrap(step+rnd(i, 26), 12); {
|
||||
case l == 2 && t < 2:
|
||||
g.Rect(x-1, y, 3, 1, s)
|
||||
g.Rect(x, y-1, 1, 3, s)
|
||||
case t != 6:
|
||||
g.Dot(x, y, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type SunMoonOptions struct {
|
||||
// Width and Height are dots; zeros mean 160 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// SunMoon arcs the sun by day, the moon by night.
|
||||
func SunMoon(c *ui.Context, hour int, o SunMoonOptions) *ui.Element {
|
||||
checkHour("SunMoon", hour)
|
||||
k := pixel.K(c)
|
||||
w, h := size("SunMoon", o.Width, o.Height, 160, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { sunMoon(g, k, hour, g.Step(4)) })
|
||||
})
|
||||
}
|
||||
|
||||
// arc centers the body: low at the ends, high midway.
|
||||
func arc(w, h, hour int) (int, int) {
|
||||
t := wrap(hour-6, 12)
|
||||
horizon, top := h*3/4, 10
|
||||
x := 10 + (w-20)*(2*t+1)/24
|
||||
return x, horizon - (horizon-top)*(2*t+1)*(23-2*t)/143
|
||||
}
|
||||
|
||||
// sunMoon draws a sun with turning rays, or cratered moon.
|
||||
func sunMoon(g pixel.Canvas, k pixel.Kit, hour, step int) {
|
||||
x, y := arc(g.W, g.H, hour)
|
||||
if hour < 6 || hour >= 18 {
|
||||
g.Disc(x, y, 5, light(k))
|
||||
g.Dot(x-2, y-1, pixel.Faint)
|
||||
g.Rect(x+1, y+1, 2, 1, pixel.Faint)
|
||||
g.Dot(x+2, y-3, pixel.Faint)
|
||||
return
|
||||
}
|
||||
// Rays turn an eighth each step: straight, then slanted.
|
||||
for i := range 4 {
|
||||
dx, dy := [4]int{1, 0, -1, 0}[i], [4]int{0, 1, 0, -1}[i]
|
||||
if step%2 == 1 {
|
||||
dx, dy = [4]int{1, 1, -1, -1}[i], [4]int{1, -1, 1, -1}[i]
|
||||
}
|
||||
g.Line(x+7*dx, y+7*dy, x+8*dx, y+8*dy, pixel.Warn)
|
||||
}
|
||||
g.Disc(x, y, 5, pixel.Warn)
|
||||
g.Rect(x-2, y-2, 2, 2, light(k))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type VignetteOptions struct {
|
||||
// Slot darkens the edges; zero means the darkest slot.
|
||||
Slot pixel.Slot
|
||||
}
|
||||
|
||||
// Vignette dithers a slot inward from edges; corners go solid.
|
||||
func Vignette(c *ui.Context, o VignetteOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
s := slotOr(o.Slot, dark(k))
|
||||
return cover(k, ui.Column(c), func(g pixel.Canvas) { vignette(g, s) }, content)
|
||||
}
|
||||
|
||||
// vignette's level grows with the elliptic distance out.
|
||||
func vignette(g pixel.Canvas, s pixel.Slot) {
|
||||
level := func(x, y int) int {
|
||||
ex := float64(2*x+1-g.W) / float64(g.W)
|
||||
ey := float64(2*y+1-g.H) / float64(g.H)
|
||||
return min(16, max(0, int((ex*ex+ey*ey-0.6)*16/1.2)))
|
||||
}
|
||||
for y := range g.H {
|
||||
for x := 0; x < g.W; {
|
||||
l, end := level(x, y), x+1
|
||||
for end < g.W && level(end, y) == l {
|
||||
end++
|
||||
}
|
||||
if l > 0 {
|
||||
g.Veil(x, y, end-x, 1, s, l)
|
||||
}
|
||||
x = end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type WaterfallOptions struct {
|
||||
// Width and Height are dots; zeros mean 64 by 96.
|
||||
Width, Height int
|
||||
Label string
|
||||
}
|
||||
|
||||
// Waterfall pours between cliffs by cycling its water slots.
|
||||
func Waterfall(c *ui.Context, o WaterfallOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Waterfall", o.Width, o.Height, 64, 96)
|
||||
return box(c, k, o.Label, w, h).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Clip(0, 0, g.W, g.H, func() { waterfall(g, k, g.Step(10)) })
|
||||
})
|
||||
}
|
||||
|
||||
// waterfall lights one three-row segment in six per column.
|
||||
func waterfall(g pixel.Canvas, k pixel.Kit, step int) {
|
||||
l, r, pool := g.W/4, g.W-g.W/4, g.H-g.H/6
|
||||
rock, lit := pick(k, pixel.Faint, pixel.Face), pick(k, pixel.Shade, pixel.Lit)
|
||||
g.Rect(0, 0, l, g.H, rock)
|
||||
g.Rect(r, 0, g.W-r, g.H, rock)
|
||||
for i := range g.H / 3 {
|
||||
x := rnd(i, 110) % max(1, l-2)
|
||||
if i%2 == 1 {
|
||||
x += r + 1
|
||||
}
|
||||
g.HLine(x, rnd(i, 111)%pool, 2, lit)
|
||||
}
|
||||
g.Rect(l, 0, r-l, pool, pixel.Info)
|
||||
// Segments take the band turned back each step: falling.
|
||||
band := pixel.Cycle([]pixel.Slot{light(k), pixel.Info, pixel.Info, pixel.Info, pixel.Info, pixel.Info}, -step)
|
||||
for x := l; x < r; x++ {
|
||||
off := rnd(x, 112) % 6
|
||||
for seg := 0; 3*seg < pool; seg++ {
|
||||
if s := band[(seg+off)%6]; s != pixel.Info {
|
||||
g.VLine(x, 3*seg, min(3, pool-3*seg), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Foam churns where it lands; ripples run outward.
|
||||
g.Rect(0, pool, g.W, g.H-pool, pixel.Info)
|
||||
g.Veil(l-3, pool, r-l+6, 3, light(k), 6+4*wrap(step, 2))
|
||||
for i := range 2 {
|
||||
d := wrap(step/2+4*i, 8)
|
||||
g.HLine(l-6-2*d, pool+4+2*i, 3, light(k))
|
||||
g.HLine(r+3+2*d, pool+4+2*i, 3, light(k))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -85,7 +85,7 @@ func TestSearch(t *testing.T) {
|
||||
t.Fatal("/ did not focus search")
|
||||
}
|
||||
tt.Type("dith")
|
||||
if !tt.HasText("1 results") || !tt.HasText("Dither") {
|
||||
if !tt.HasText("2 results") || !tt.HasText("Dither") || !tt.HasText("FadeDither") {
|
||||
t.Fatalf("results: %q", tt.Texts())
|
||||
}
|
||||
tt.Key(0, ui.KeyP)
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ func (a *App) sidebar(c *ui.Context) {
|
||||
g.VLine(g.W-2, 0, g.H, pixel.Shade)
|
||||
g.VLine(g.W-1, 0, g.H, pixel.Edge)
|
||||
}).Children(func() {
|
||||
ui.Column(c).Padding(k.D(4)).Gap(k.D(1)).Children(func() {
|
||||
ui.Column(c).Padding(k.D(4)).Gap(k.D(1)).FocusGroup(ui.Vertical).Children(func() {
|
||||
for _, cat := range Categories {
|
||||
n := count(cat.Key)
|
||||
path := "/cat/" + cat.Key
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/fx"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var fxEntries = []Entry{
|
||||
{291, "PaletteCycle", "调色板循环", "fx", "Four slots take turns, so the rings seem to flow.", demoPaletteCycle},
|
||||
{292, "Sky", "天空", "fx", "Blue by day; by night, stars that blink.", demoSky},
|
||||
{293, "SunMoon", "日月", "fx", "Up at six, down at six. The moon takes the night shift.", demoSunMoon},
|
||||
{294, "Clouds", "云", "fx", "Far clouds dawdle, near clouds hurry.", demoClouds},
|
||||
{295, "Starfield", "星空", "fx", "Three layers of stars rush past. Hyperspace at 12 fps.", demoStarfield},
|
||||
{296, "Rain", "雨", "fx", "Slanted streaks, each landing with a tiny splash.", demoRain},
|
||||
{297, "Snow", "雪", "fx", "Flakes sway down; near ones are big and quick.", demoSnow},
|
||||
{298, "Petals", "樱花瓣", "fx", "Petals ride the breeze, turning as they fall.", demoPetals},
|
||||
{299, "Leaves", "落叶", "fx", "Leaves swing side to side on the way down.", demoLeaves},
|
||||
{300, "Fireflies", "萤火虫", "fx", "Little lights wander, glow, and rest.", demoFireflies},
|
||||
{301, "Waterfall", "瀑布", "fx", "Four slots cycling down make the water fall.", demoWaterfallFx},
|
||||
{302, "Ocean", "海浪", "fx", "Wave bands roll in by cycling, with glints on top.", demoOcean},
|
||||
{303, "Fire", "火焰", "fx", "Three flicker frames, heat rising through them.", demoFire},
|
||||
{304, "Lava", "熔岩", "fx", "Hot bands flow; bubbles swell and pop. Do not touch.", demoLava},
|
||||
{305, "Aurora", "极光", "fx", "Curtains of light, their colors sliding sideways.", demoAurora},
|
||||
{306, "Lightning", "闪电", "fx", "A flash, then a forked bolt. No two strikes alike.", demoLightning},
|
||||
{307, "Mountains", "山脉", "fx", "Three ridges scroll at three speeds. Parallax!", demoMountains},
|
||||
{308, "SeasonScene", "四季场景", "fx", "Pick a season and an hour; the world repaints itself.", demoSeasonScene},
|
||||
}
|
||||
|
||||
type fxDemo struct {
|
||||
season, hour int
|
||||
strike, shake, flash, mosaic, glitch, boom int
|
||||
fade, iris, dissolve bool
|
||||
}
|
||||
|
||||
func fxState(c *ui.Context) *fxDemo {
|
||||
p := pixel.K(c).Palette()
|
||||
return ui.Local(c.Root(), demoKey("fx"), func() fxDemo { return fxDemo{season: int(p.Season), hour: hourOf(p)} })
|
||||
}
|
||||
|
||||
// hourOf suits p: morning by day, late evening by night.
|
||||
func hourOf(p *pixel.Palette) int {
|
||||
if p.Night {
|
||||
return 21
|
||||
}
|
||||
return 10
|
||||
}
|
||||
|
||||
// paletteOf is the settings in force with season's palette.
|
||||
func paletteOf(c *ui.Context, s pixel.Season, night bool) pixel.Settings {
|
||||
set := pixel.Current(c)
|
||||
set.Palette = pixel.Find(s, night)
|
||||
return set
|
||||
}
|
||||
|
||||
// over stacks an effect on a same-sized sky.
|
||||
func over(c *ui.Context, w, h int, effect func()) {
|
||||
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: w, Height: h}) }, effect)
|
||||
}
|
||||
|
||||
// portalBand rings the portal, outside in.
|
||||
var portalBand = []pixel.Slot{pixel.Accent, pixel.AccentLit, pixel.Magic, pixel.Info}
|
||||
|
||||
var portal = pixel.SpriteOf(15, 15, func(x, y int) (pixel.Slot, bool) {
|
||||
d := math.Hypot(float64(x-7), float64(y-7))
|
||||
return portalBand[int(d)%len(portalBand)], d < 7.5
|
||||
})
|
||||
|
||||
func demoPaletteCycle(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
||||
fx.PaletteCycle(c, portal, portalBand, fx.PaletteCycleOptions{Scale: 3, Label: "portal"})
|
||||
fx.PaletteCycle(c, portal, portalBand, fx.PaletteCycleOptions{Scale: 2, FPS: 4, Label: "slow portal"})
|
||||
})
|
||||
}
|
||||
|
||||
func demoSky(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Row(c).Gap(k.D(4)).Children(func() {
|
||||
for _, night := range []bool{false, true} {
|
||||
pixel.Scope(c, paletteOf(c, k.Palette().Season, night), func() {
|
||||
fx.Sky(c, fx.SkyOptions{Width: 68, Height: 64, Label: map[bool]string{false: "day sky", true: "night sky"}[night]})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoSunMoon(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
pixel.Scope(c, paletteOf(c, k.Palette().Season, s.hour < 6 || s.hour >= 18), func() {
|
||||
over(c, 140, 72, func() {
|
||||
fx.SunMoon(c, s.hour, fx.SunMoonOptions{Width: 140, Height: 72, Label: fmt.Sprintf("%02d:00", s.hour)})
|
||||
})
|
||||
})
|
||||
if input.Stepper(c, "Hour", &s.hour, input.StepperOptions{Max: 23}).Changed {
|
||||
log(fmt.Sprintf("hour %02d", s.hour))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoClouds(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Clouds(c, fx.CloudsOptions{Width: 140, Height: 80, Label: "clouds"}) })
|
||||
}
|
||||
|
||||
func demoStarfield(c *ui.Context, log func(string)) {
|
||||
fx.Starfield(c, fx.StarfieldOptions{Width: 140, Height: 80, Label: "stars"})
|
||||
}
|
||||
|
||||
func demoRain(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Rain(c, fx.RainOptions{Width: 140, Height: 80, Label: "rain"}) })
|
||||
}
|
||||
|
||||
func demoSnow(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Snow(c, fx.SnowOptions{Width: 140, Height: 80, Label: "snow"}) })
|
||||
}
|
||||
|
||||
func demoPetals(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Petals(c, fx.PetalsOptions{Width: 140, Height: 80, Label: "petals"}) })
|
||||
}
|
||||
|
||||
func demoLeaves(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Leaves(c, fx.LeavesOptions{Width: 140, Height: 80, Label: "leaves"}) })
|
||||
}
|
||||
|
||||
func demoFireflies(c *ui.Context, log func(string)) {
|
||||
pixel.Scope(c, paletteOf(c, pixel.K(c).Palette().Season, true), func() {
|
||||
over(c, 140, 80, func() { fx.Fireflies(c, fx.FirefliesOptions{Width: 140, Height: 80, Label: "fireflies"}) })
|
||||
})
|
||||
}
|
||||
|
||||
func demoWaterfallFx(c *ui.Context, log func(string)) {
|
||||
fx.Waterfall(c, fx.WaterfallOptions{Label: "waterfall"})
|
||||
}
|
||||
|
||||
func demoOcean(c *ui.Context, log func(string)) {
|
||||
fx.Ocean(c, fx.OceanOptions{Width: 140, Label: "ocean"})
|
||||
}
|
||||
|
||||
func demoFire(c *ui.Context, log func(string)) {
|
||||
fx.Fire(c, fx.FireOptions{Scale: 3, Label: "campfire"})
|
||||
}
|
||||
|
||||
func demoLava(c *ui.Context, log func(string)) {
|
||||
fx.Lava(c, fx.LavaOptions{Width: 140, Label: "lava"})
|
||||
}
|
||||
|
||||
func demoAurora(c *ui.Context, log func(string)) {
|
||||
pixel.Scope(c, paletteOf(c, pixel.K(c).Palette().Season, true), func() {
|
||||
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: 140, Height: 80}) },
|
||||
func() { fx.Aurora(c, fx.AuroraOptions{Width: 140, Height: 80, Label: "aurora"}) },
|
||||
func() { fx.Mountains(c, fx.MountainsOptions{Width: 140, Height: 80}) })
|
||||
})
|
||||
}
|
||||
|
||||
func demoLightning(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
pixel.Scope(c, paletteOf(c, k.Palette().Season, true), func() {
|
||||
over(c, 120, 80, func() { fx.Lightning(c, fx.LightningOptions{Cue: s.strike, Width: 120, Height: 80, Label: "storm"}) })
|
||||
})
|
||||
if small(c, "STRIKE", false) {
|
||||
s.strike++
|
||||
log(fmt.Sprintf("strike %d", s.strike))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoMountains(c *ui.Context, log func(string)) {
|
||||
over(c, 140, 80, func() { fx.Mountains(c, fx.MountainsOptions{Width: 140, Height: 80, Label: "mountains"}) })
|
||||
}
|
||||
|
||||
func demoSeasonScene(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
fx.SeasonScene(c, pixel.Season(s.season), s.hour, fx.SeasonSceneOptions{Width: 144, Height: 80, Label: "the world"})
|
||||
seasons := []string{"Spring", "Summer", "Autumn", "Winter"}
|
||||
if input.Segmented(c, "Season", seasons, &s.season, input.SegmentedOptions{}).Changed {
|
||||
log("season " + seasons[s.season])
|
||||
}
|
||||
if input.Stepper(c, "Hour", &s.hour, input.StepperOptions{Max: 23}).Changed {
|
||||
log(fmt.Sprintf("hour %02d", s.hour))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/fx"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var screenFxEntries = []Entry{
|
||||
{309, "Scanlines", "扫描线", "fx", "Every other row, half shaded. Instant CRT.", demoScanlines},
|
||||
{310, "Vignette", "暗角", "fx", "The corners dither into the dark.", demoVignette},
|
||||
{311, "ScreenShake", "震屏", "fx", "Something big just landed.", demoScreenShake},
|
||||
{312, "Flash", "闪白", "fx", "A step of light, then half a step. Never under Reduce Motion.", demoFlash},
|
||||
{313, "FadeDither", "抖动淡出", "fx", "Fades through Bayer levels, never through opacity.", demoFadeDither},
|
||||
{314, "IrisWipe", "光圈转场", "fx", "The circle closes on the middle. That's all, folks.", demoIrisWipe},
|
||||
{315, "Dissolve", "溶解", "fx", "Dots give up one batch at a time, in shuffled order.", demoDissolve},
|
||||
{316, "Mosaic", "马赛克", "fx", "Blocks grow to eight dots, then the picture comes back.", demoMosaic},
|
||||
{317, "Glitch", "故障", "fx", "Rows slip and two colors trade places. Blow on the cartridge.", demoGlitch},
|
||||
{318, "Sparkle", "闪光", "fx", "Four stars take turns around a shiny thing.", demoSparkle},
|
||||
{319, "Explosion", "爆炸", "fx", "Fireball, smoke, debris. Stand back.", demoExplosion},
|
||||
{320, "Confetti", "彩纸", "fx", "Bits of every color tumble down. You won!", demoConfetti},
|
||||
}
|
||||
|
||||
// stage is a little world in the palette's season.
|
||||
func stage(c *ui.Context) {
|
||||
p := pixel.K(c).Palette()
|
||||
fx.SeasonScene(c, p.Season, hourOf(p), fx.SeasonSceneOptions{Width: 128, Height: 72, Label: "stage"})
|
||||
}
|
||||
|
||||
func demoScanlines(c *ui.Context, log func(string)) {
|
||||
ui.Column(c).AlignItems(ui.Start).Children(func() {
|
||||
fx.Scanlines(c, fx.ScanlinesOptions{}, func() { stage(c) })
|
||||
})
|
||||
}
|
||||
|
||||
func demoVignette(c *ui.Context, log func(string)) {
|
||||
ui.Column(c).AlignItems(ui.Start).Children(func() {
|
||||
fx.Vignette(c, fx.VignetteOptions{}, func() { stage(c) })
|
||||
})
|
||||
}
|
||||
|
||||
func demoScreenShake(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).AlignItems(ui.Start).Children(func() {
|
||||
fx.ScreenShake(c, fx.ScreenShakeOptions{Cue: s.shake}, func() { stage(c) })
|
||||
if small(c, "SHAKE", false) {
|
||||
s.shake++
|
||||
log("rumble!")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoFlash(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).AlignItems(ui.Start).Children(func() {
|
||||
fx.Flash(c, fx.FlashOptions{Cue: s.flash}, func() { stage(c) })
|
||||
if small(c, "FLASH", false) {
|
||||
s.flash++
|
||||
log("flash")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// toggled shows content under a transition, with its toggle.
|
||||
func toggled(c *ui.Context, text string, on *bool, log func(string), effect func(content func())) {
|
||||
k := pixel.K(c)
|
||||
ui.Column(c).Gap(k.D(6)).AlignItems(ui.Start).Children(func() {
|
||||
effect(func() { stage(c) })
|
||||
if input.Toggle(c, text, on, input.ToggleOptions{Size: input.Small}).Changed {
|
||||
log(fmt.Sprintf("%s %v", text, *on))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoFadeDither(c *ui.Context, log func(string)) {
|
||||
s := fxState(c)
|
||||
toggled(c, "FADE OUT", &s.fade, log, func(content func()) { fx.FadeDither(c, s.fade, fx.FadeDitherOptions{}, content) })
|
||||
}
|
||||
|
||||
func demoIrisWipe(c *ui.Context, log func(string)) {
|
||||
s := fxState(c)
|
||||
toggled(c, "CLOSE", &s.iris, log, func(content func()) { fx.IrisWipe(c, s.iris, fx.IrisWipeOptions{}, content) })
|
||||
}
|
||||
|
||||
func demoDissolve(c *ui.Context, log func(string)) {
|
||||
s := fxState(c)
|
||||
toggled(c, "DISSOLVE", &s.dissolve, log, func(content func()) { fx.Dissolve(c, s.dissolve, fx.DissolveOptions{}, content) })
|
||||
}
|
||||
|
||||
func demoMosaic(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
||||
fx.Mosaic(c, display.Reduce(sunset, 48, 32, k.Palette()), fx.MosaicOptions{Cue: s.mosaic, Scale: 2, Label: "sunset"})
|
||||
fx.Mosaic(c, slime, fx.MosaicOptions{Cue: s.mosaic, Scale: 3, Label: "slime"})
|
||||
})
|
||||
if small(c, "PIXELATE", false) {
|
||||
s.mosaic++
|
||||
log("pixelate")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoGlitch(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
||||
fx.Glitch(c, display.Reduce(sunset, 48, 32, k.Palette()), fx.GlitchOptions{Cue: s.glitch, Scale: 2, Label: "sunset"})
|
||||
fx.Glitch(c, slime, fx.GlitchOptions{Cue: s.glitch, Scale: 3, Label: "slime"})
|
||||
})
|
||||
if small(c, "GLITCH", false) {
|
||||
s.glitch++
|
||||
log("glitch")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoSparkle(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
layout.Layers(c, func() {
|
||||
ui.Box(c).Size(k.D(48), k.D(48)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
|
||||
display.Gem(c, display.Legendary, display.GemOptions{Scale: 2})
|
||||
})
|
||||
}, func() { fx.Sparkle(c, fx.SparkleOptions{Size: 48, Label: "sparkles"}) })
|
||||
}
|
||||
|
||||
func demoExplosion(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := fxState(c)
|
||||
ui.Column(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
|
||||
fx.Explosion(c, fx.ExplosionOptions{Cue: s.boom, Label: "blast"})
|
||||
if small(c, "BOOM", false) {
|
||||
s.boom++
|
||||
log("boom")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoConfetti(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
layout.Layers(c, func() {
|
||||
ui.Box(c).Size(k.D(140), k.D(64)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
|
||||
pixel.Text(c, "YOU WIN!", pixel.TextOptions{Scale: 2, Slot: pixel.Accent})
|
||||
})
|
||||
}, func() { fx.Confetti(c, fx.ConfettiOptions{Width: 140, Height: 64, Label: "confetti"}) })
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func demoLoadingTips(c *ui.Context, log func(string)) {
|
||||
func demoLetterbox(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := screenState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
ui.Column(c).Gap(k.D(6)).AlignItems(ui.Start).Children(func() {
|
||||
game.Letterbox(c, s.cutscene, game.LetterboxOptions{}, func() {
|
||||
ui.Box(c).Size(k.D(200), k.D(80)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestFxDemosLogTheirActions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
steps []string
|
||||
}{
|
||||
{"/c/293-sunmoon", []string{"Hour", "Right"}}, {"/c/306-lightning", []string{"STRIKE"}},
|
||||
{"/c/308-seasonscene", []string{"Winter", "Hour", "Right"}}, {"/c/311-screenshake", []string{"SHAKE"}},
|
||||
{"/c/312-flash", []string{"FLASH"}}, {"/c/313-fadedither", []string{"FADE OUT"}}, {"/c/314-iriswipe", []string{"CLOSE"}},
|
||||
{"/c/315-dissolve", []string{"DISSOLVE"}}, {"/c/316-mosaic", []string{"PIXELATE"}}, {"/c/317-glitch", []string{"GLITCH"}},
|
||||
{"/c/319-explosion", []string{"BOOM"}},
|
||||
} {
|
||||
a, tt := open(tc.path)
|
||||
slug := tc.path[3:]
|
||||
for _, step := range tc.steps {
|
||||
before := len(a.logs[slug])
|
||||
if step == "Right" {
|
||||
tt.Key(0, ui.KeyRight)
|
||||
} else {
|
||||
click(t, tt, step)
|
||||
}
|
||||
// Focusing the stepper changes nothing worth a line.
|
||||
if len(a.logs[slug]) <= before && step != "Hour" {
|
||||
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupsAreOneTabStopEach(t *testing.T) {
|
||||
for _, page := range []struct{ path, query string }{{"/", ""}, {"/cat/fx", ""}, {"/", "e"}} {
|
||||
_, tt := open(page.path)
|
||||
tt.Key(0, ui.KeySlash)
|
||||
tt.Type(page.query)
|
||||
// Search, four top-bar controls, then one stop per group.
|
||||
stops := 1
|
||||
for tt.Key(0, ui.KeyTab); !tt.Focused("Search components"); tt.Key(0, ui.KeyTab) {
|
||||
if stops++; stops > 12 {
|
||||
t.Fatalf("%s %q: a Tab cycle passes over 12 stops", page.path, page.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, tt := open("/")
|
||||
for range 40 {
|
||||
if tt.Focused("Spring Day, in use") {
|
||||
break
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
}
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if !tt.Focused("Spring Night") {
|
||||
t.Fatal("Right did not move along the palette strip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeSceneFollowsThePaletteStrip(t *testing.T) {
|
||||
a, tt := open("/")
|
||||
if _, ok := tt.Find("Spring Day scene"); !ok {
|
||||
t.Fatalf("no spring scene in %q", tt.Texts())
|
||||
}
|
||||
click(t, tt, "Winter Night")
|
||||
if a.palette != 7 {
|
||||
t.Fatalf("the strip chose palette %d", a.palette)
|
||||
}
|
||||
r, ok := tt.Find("Winter Night scene")
|
||||
if !ok {
|
||||
t.Fatalf("the scene kept its season: %q", tt.Texts())
|
||||
}
|
||||
// The banner's sky starts in Winter Night's darkest slot.
|
||||
got, want := tt.Image().RGBAAt(int(r.X)+1, int(r.Y)+1), pixel.WinterNight.Colors[pixel.Well]
|
||||
if got.R != want.R || got.G != want.G || got.B != want.B {
|
||||
t.Fatalf("the banner's sky is %v, want %v", got, want)
|
||||
}
|
||||
if _, ok := tt.Find("Winter Night, in use"); !ok {
|
||||
t.Fatal("the strip does not mark the palette in use")
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,8 @@ func TestInvariantsPages(t *testing.T) {
|
||||
a.Go(path)
|
||||
}
|
||||
allowed := colors(pal)
|
||||
if name == "entry" {
|
||||
// Home shows every palette in its strip.
|
||||
if name == "entry" || name == "home" {
|
||||
allowed = colors(pixel.Palettes...)
|
||||
}
|
||||
img := render(a.View, 640, 400)
|
||||
|
||||
+48
-4
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/feedback"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/fx"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
@@ -49,13 +50,23 @@ func page(c *ui.Context, fn func()) {
|
||||
|
||||
func (a *App) home(c *ui.Context) {
|
||||
k := pixel.K(c)
|
||||
text.ShadowText(c, logo, text.ShadowTextOptions{Slot: pixel.Accent, Scale: 3})
|
||||
p := k.Palette()
|
||||
// The banner lives in the palette's own season and hour.
|
||||
layout.Layers(c, func() {
|
||||
fx.SeasonScene(c, p.Season, hourOf(p), fx.SeasonSceneOptions{Height: 96, Label: p.Name + " scene"}).FillWidth()
|
||||
}, func() {
|
||||
ui.Column(c).Padding(k.D(10)).Children(func() {
|
||||
text.OutlineText(c, logo, text.OutlineTextOptions{Slot: pixel.Accent, Scale: 3})
|
||||
})
|
||||
})
|
||||
pixel.Text(c, "Pixel components for MyGo. Every dot sits on the grid; every color comes from sixteen.", pixel.TextOptions{Wrap: true})
|
||||
pixel.Text(c, "为 MyGo 打造的像素组件。每个点落在点阵上,每种颜色来自十六格调色板。", pixel.TextOptions{Wrap: true, Slot: pixel.Faint})
|
||||
comps := len(Entries()) - count("foundations")
|
||||
stats := fmt.Sprintf("%d COMPONENTS %d CATEGORIES %d PALETTES", comps, len(Categories)-1, len(pixel.Palettes))
|
||||
text.MicroText(c, stats, text.MicroTextOptions{Slot: pixel.Accent})
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).Children(func() {
|
||||
a.palettes(c)
|
||||
// Each grid is one Tab stop; arrows move inside it.
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).FocusGroup(ui.Horizontal | ui.Vertical).Children(func() {
|
||||
for _, cat := range Categories {
|
||||
n := count(cat.Key)
|
||||
if press(c, 0, 40, cat.Name, func(g pixel.Canvas, st pixel.State) {
|
||||
@@ -76,6 +87,39 @@ func (a *App) home(c *ui.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// palettes shows each palette in its colors; press to pick.
|
||||
func (a *App) palettes(c *ui.Context) {
|
||||
k := pixel.K(c)
|
||||
ui.Grid(c).Columns(4).Gap(k.D(4)).FocusGroup(ui.Horizontal | ui.Vertical).Children(func() {
|
||||
for i, pal := range pixel.Palettes {
|
||||
name := pal.Name
|
||||
if i == a.palette {
|
||||
name += ", in use"
|
||||
}
|
||||
set := pixel.Current(c)
|
||||
set.Palette = pal
|
||||
pixel.Scope(c, set, func() {
|
||||
if press(c, 0, 30, name, func(g pixel.Canvas, st pixel.State) {
|
||||
fill := pixel.Face
|
||||
if i == a.palette || st.Hover {
|
||||
fill = pixel.Lit
|
||||
}
|
||||
g.Frame(0, 0, g.W, g.H, pixel.Window, fill)
|
||||
for s := range pixel.Slots {
|
||||
g.Rect(5+3*s, 4, 3, 8, pixel.Slot(s))
|
||||
}
|
||||
g.Text(5, 14, pal.Name+" "+pal.Zh, pixel.Ink, 1)
|
||||
if i == a.palette {
|
||||
g.Rect(g.W-9, 5, 4, 4, pixel.Accent)
|
||||
}
|
||||
}) {
|
||||
a.setPalette(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) category(c *ui.Context, key string) {
|
||||
var cat Category
|
||||
for _, x := range Categories {
|
||||
@@ -93,7 +137,7 @@ func (a *App) category(c *ui.Context, key string) {
|
||||
text.ShadowText(c, cat.Name, text.ShadowTextOptions{Scale: 2})
|
||||
pixel.Text(c, cat.Zh, pixel.TextOptions{Slot: pixel.Faint})
|
||||
})
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).Children(func() {
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).FocusGroup(ui.Horizontal | ui.Vertical).Children(func() {
|
||||
for _, e := range Entries() {
|
||||
if e.Category == key {
|
||||
a.card(c, e)
|
||||
@@ -180,7 +224,7 @@ func (a *App) results(c *ui.Context) {
|
||||
found := matches(a.query)
|
||||
pixel.Text(c, fmt.Sprintf("%d results for %q", len(found), strings.TrimSpace(a.query)), pixel.TextOptions{Slot: pixel.Faint})
|
||||
k := pixel.K(c)
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).Children(func() {
|
||||
ui.Grid(c).Columns(3).Gap(k.D(4)).FocusGroup(ui.Horizontal | ui.Vertical).Children(func() {
|
||||
for _, e := range found {
|
||||
a.card(c, e)
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ var Categories = []Category{
|
||||
{"tabletop", "Tabletop", "桌游", icon.Crown},
|
||||
}
|
||||
|
||||
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries)
|
||||
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries)
|
||||
|
||||
func Entries() []Entry { return entries }
|
||||
|
||||
|
||||
+1
-35
@@ -1,8 +1,6 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
|
||||
@@ -14,38 +12,13 @@ type LetterboxOptions struct {
|
||||
Bar int
|
||||
}
|
||||
|
||||
type barsKey struct{}
|
||||
|
||||
// bars is the last on, its change time and step.
|
||||
type bars struct {
|
||||
on bool
|
||||
at time.Time
|
||||
from int
|
||||
}
|
||||
|
||||
// Letterbox slides cutscene bars over content in four steps.
|
||||
func Letterbox(c *ui.Context, on bool, o LetterboxOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
bar := pixel.Or("Letterbox Bar", o.Bar, 16)
|
||||
var cover *ui.Element
|
||||
e := layout.Layers(c, content, func() { cover = ui.Box(c).Fill().PassThrough() })
|
||||
// First shown, the bars are already where on puts them.
|
||||
s := ui.Local(e, barsKey{}, func() bars { return bars{on: on, from: 4 * boolInt(on)} })
|
||||
// shown is the step, 0–4; turns start where it is.
|
||||
shown := func() int {
|
||||
if k.Reduce() {
|
||||
return 4 * boolInt(s.on)
|
||||
}
|
||||
if n := k.Since(c, s.at, 8, 4); s.on {
|
||||
return min(4, s.from+n)
|
||||
} else {
|
||||
return max(0, s.from-n)
|
||||
}
|
||||
}
|
||||
if on != s.on {
|
||||
s.from, s.on, s.at = shown(), on, k.Now()
|
||||
}
|
||||
h := bar * shown() / 4
|
||||
h := bar * k.Toward(c, e, on, 8, 4) / 4
|
||||
cover.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Rect(0, 0, g.W, h, pixel.Ink)
|
||||
@@ -53,10 +26,3 @@ func Letterbox(c *ui.Context, on bool, o LetterboxOptions, content func()) *ui.E
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func boolInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+1
-4
@@ -128,8 +128,5 @@ func arriving(c *ui.Context, home *ui.Element, open bool) int {
|
||||
if open {
|
||||
on = 1
|
||||
}
|
||||
if k.Reduce() {
|
||||
return 0
|
||||
}
|
||||
return 4 - k.Since(c, k.Cue(home, on), 15, 4)
|
||||
return 4 - k.Shot(c, home, on, 15, 4)
|
||||
}
|
||||
@@ -54,6 +54,25 @@ func TestDrawerSlidesInInSteps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawerOpenedUnderReduceMotionNeverSlides(t *testing.T) {
|
||||
clk, open := newClock(), false
|
||||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||||
overlay.Drawer(c, "Menu", &open, overlay.DrawerOptions{Side: display.Right, Size: 100}, func() {
|
||||
pixel.Text(c, "Items", pixel.TextOptions{})
|
||||
})
|
||||
})
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
open = true
|
||||
// At rest the panel's text sits at 640-200+16.
|
||||
for _, reduce := range []bool{true, false} {
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1})
|
||||
tt.Frame()
|
||||
if x := find(t, tt, "Items").X; x != 456 {
|
||||
t.Fatalf("reduce %v: the drawer's text is at %v", reduce, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetClosesOnALongPull(t *testing.T) {
|
||||
open, closed := true, 0
|
||||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||||
|
||||
@@ -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