Add fx components 291-320 and the home scene; Kit.Shot, Kit.Toward

Palette cycling, sky, sun and moon, clouds, starfield, rain, snow,
petals, leaves, fireflies, waterfall, ocean, campfire, lava, aurora,
lightning, mountains and a season scene; scanlines, vignette, screen
shake, flash, dither fades, iris wipes, dissolves, mosaics, glitches,
sparkles, explosions and confetti. Home opens on a live scene in the
palette's season and a strip of all eight palettes.

pixel adds Canvas.Cycled, Sprite.At, SpriteOf, Kit.Toward and
Kit.Shot, which spends a cue under Reduce Motion so it never replays;
display exports Reduce. Gallery grids and the sidebar are one Tab
stop each.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 04:05:25 -04:00
1 parent 86a6901333
commit 8be66c09f2
56 files changed
+3324 -101

No files matched your search

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