A cross-cutting pass, each fix with a test that failed first. Veils: MyGo still hands a key to a base that turned off this frame, so every element key now goes through pixel.Shortcut, which an archtest enforces. Input handlers record and the build applies them only while enabled; the editor turns away a veil-frame letter or copy; value bases get a copy and keep only enabled turns. Popups give a held focus back to their opener when the veil lifts. Motion: S5 is enforced at the clock. Every Tick, Since, Step, Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps, fast animations step at 15, and drawn time is quantized to frames. Clocks stepping back: Since holds at zero without overflow, and k.Elapsed moves a stored start to now, so nothing goes negative, runs backward or indexes out of range. Also: logs count typed text instead of holding it, gallery names use glyphs the font has, removable rows are keyed, components take their own Options (archtest), and sweeps over all 508 demos check veiled keys, clicks, wheels, the clock stepping back and names.
229 lines
7.5 KiB
Go
229 lines
7.5 KiB
Go
package gallery
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/media"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
var mediaEntries = []Entry{
|
|
{346, "AudioPlayer", "音乐播放器", "media", "A whole deck in one box. Stop rewinds to the top.", demoAudioPlayer},
|
|
{347, "Transport", "播放控制", "media", "Back, play or pause, stop, skip: the four buttons every deck agrees on.", demoTransport},
|
|
{348, "Scrubber", "进度定位", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
|
|
{349, "Volume", "音量", "media", "Eight bars; click one or use arrows. The speaker mutes.", demoVolume},
|
|
{350, "Equalizer", "均衡器", "media", "Five faders, ±12 dB. Bass boost at your own risk.", demoEqualizer},
|
|
{351, "Visualizer", "频谱", "media", "Bars bounce out from the middle, hottest at the tips.", demoVisualizer},
|
|
{352, "Playlist", "播放列表", "media", "♪ marks the track that plays. Enter plays the one you pick.", demoPlaylist},
|
|
{353, "Cassette", "磁带", "media", "Reels turn while it plays; watch the tape cross over.", demoCassette},
|
|
{354, "Cartridge", "卡带", "media", "A cart with a sticker. Blow on the contacts before use.", demoCartridge},
|
|
{355, "Vinyl", "黑胶", "media", "Turns in eight frames; the arm swings onto the groove.", demoVinyl},
|
|
}
|
|
|
|
type mediaDemo struct {
|
|
player media.Player
|
|
track, pick int
|
|
deck string
|
|
seek time.Duration
|
|
level int
|
|
muted bool
|
|
gains []float64
|
|
tape, spin, video bool
|
|
tapeAt, videoAt time.Duration
|
|
notes []string
|
|
beat bool
|
|
pattern [][16]bool
|
|
rows []media.Row
|
|
row, chosen int
|
|
split, mhz float64
|
|
rec bool
|
|
ran, taped, filmed time.Time
|
|
}
|
|
|
|
var tracks = []media.Track{
|
|
{Title: "Overworld Theme", Artist: "The Square Waves", Length: 2*time.Minute + 47*time.Second},
|
|
{Title: "Boss Battle", Artist: "Chip Wizards", Length: 3*time.Minute + 12*time.Second},
|
|
{Title: "Save Point Lullaby", Artist: "Triangle Wave", Length: 1*time.Minute + 58*time.Second},
|
|
{Title: "Credits Roll", Artist: "Pulse & Noise", Length: 4*time.Minute + 5*time.Second},
|
|
}
|
|
|
|
func mediaState(c *ui.Context) *mediaDemo {
|
|
return ui.Local(c.Root(), demoKey("media"), func() mediaDemo {
|
|
return mediaDemo{
|
|
player: media.Player{Level: 6}, pick: 1, deck: "STOPPED", seek: 72 * time.Second, level: 5,
|
|
gains: []float64{6, 2, 0, 3, 5},
|
|
pattern: [][16]bool{beat("x...x...x...x..."), beat("....x.......x..."), beat("x.x.x.x.x.x.x.xx"), beat("............x...")},
|
|
rows: melody(), split: 0.5, mhz: 98.3,
|
|
}
|
|
})
|
|
}
|
|
|
|
// beat reads a pattern: x hits, . rests.
|
|
func beat(s string) [16]bool {
|
|
var b [16]bool
|
|
for i, r := range s {
|
|
b[i] = r == 'x'
|
|
}
|
|
return b
|
|
}
|
|
|
|
// spin moves *at while playing, as an audio engine would.
|
|
func spin(c *ui.Context, last *time.Time, playing bool, at *time.Duration, length time.Duration) {
|
|
k := pixel.K(c)
|
|
if playing && !last.IsZero() {
|
|
// A clock stepping back plays nothing backward.
|
|
*at = (*at + max(0, k.Now().Sub(*last))) % length
|
|
}
|
|
*last = time.Time{}
|
|
if playing {
|
|
*last = k.Now()
|
|
k.Tick(c, 4)
|
|
}
|
|
}
|
|
|
|
func demoAudioPlayer(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
t := tracks[s.track]
|
|
spin(c, &s.ran, s.player.Playing, &s.player.At, t.Length)
|
|
ui.Box(c).Width(k.D(240)).Children(func() {
|
|
res := media.AudioPlayer(c, "Now playing", t, &s.player, media.AudioPlayerOptions{})
|
|
switch {
|
|
case res.Previous:
|
|
s.track = (s.track + len(tracks) - 1) % len(tracks)
|
|
case res.Next:
|
|
s.track = (s.track + 1) % len(tracks)
|
|
}
|
|
if res.Previous || res.Next {
|
|
s.player.At = 0
|
|
log("♪ " + tracks[s.track].Title)
|
|
}
|
|
if res.Changed {
|
|
log(fmt.Sprintf("playing %v at %s, volume %d, muted %v", s.player.Playing, s.player.At.Truncate(time.Second), s.player.Level, s.player.Muted))
|
|
}
|
|
})
|
|
}
|
|
|
|
func demoTransport(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
ui.Column(c).Gap(k.D(6)).Children(func() {
|
|
playing := s.deck == "PLAYING"
|
|
res := media.Transport(c, "Deck", &playing, media.TransportOptions{})
|
|
switch {
|
|
case res.Stopped:
|
|
s.deck = "STOPPED"
|
|
case res.Changed && playing:
|
|
s.deck = "PLAYING"
|
|
case res.Changed:
|
|
s.deck = "PAUSED"
|
|
case res.Previous:
|
|
log("PREVIOUS")
|
|
case res.Next:
|
|
log("NEXT")
|
|
}
|
|
if res.Changed || res.Stopped {
|
|
log(s.deck)
|
|
}
|
|
pixel.Text(c, s.deck, pixel.TextOptions{Slot: pixel.Faint})
|
|
})
|
|
}
|
|
|
|
func demoScrubber(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
if media.Scrubber(c, "Seek", &s.seek, tracks[1].Length, media.ScrubberOptions{Width: 120}).Changed {
|
|
log("seek " + s.seek.String())
|
|
}
|
|
}
|
|
|
|
func demoVolume(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
if media.Volume(c, "Volume", &s.level, &s.muted, media.VolumeOptions{}).Changed {
|
|
log(fmt.Sprintf("volume %d of 8, muted %v", s.level, s.muted))
|
|
}
|
|
}
|
|
|
|
func demoEqualizer(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
bands := []string{"60", "250", "1K", "4K", "12K"}
|
|
if b := media.Equalizer(c, "Equalizer", bands, s.gains, media.EqualizerOptions{}).Band; b >= 0 {
|
|
log(fmt.Sprintf("%s Hz %+.1f dB", bands[b], s.gains[b]))
|
|
}
|
|
}
|
|
|
|
func demoVisualizer(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
// A made-up song: each bar sways at its own pace.
|
|
t := 0.0
|
|
if !k.Reduce() {
|
|
t = float64(k.Tick(c, 12)) / 12
|
|
}
|
|
levels := make([]float64, 24)
|
|
for i := range levels {
|
|
f := float64(i)
|
|
levels[i] = (0.55 + 0.45*math.Sin(t*(2+f/5)+f)) * (1 - f/32)
|
|
}
|
|
media.Visualizer(c, "Visualizer", levels, media.VisualizerOptions{Height: 40})
|
|
}
|
|
|
|
func demoPlaylist(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
res := media.Playlist(c, "Playlist", tracks, s.track, &s.pick, media.PlaylistOptions{Height: 80})
|
|
if res.Submitted {
|
|
s.track, s.player.At = s.pick, 0
|
|
log("♪ " + tracks[s.track].Title)
|
|
} else if res.Changed {
|
|
log("picked " + tracks[s.pick].Title)
|
|
}
|
|
}
|
|
|
|
func demoCassette(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
const side = 45 * time.Second
|
|
spin(c, &s.taped, s.tape, &s.tapeAt, side)
|
|
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
|
media.Cassette(c, "Mixtape, side A", media.CassetteOptions{Title: "Mixtape 86", Playing: s.tape, Progress: float64(s.tapeAt) / float64(side)})
|
|
if input.Toggle(c, "PLAY", &s.tape, input.ToggleOptions{}).Changed {
|
|
log(fmt.Sprintf("tape playing %v", s.tape))
|
|
}
|
|
})
|
|
}
|
|
|
|
func demoCartridge(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
ui.Row(c).Gap(k.D(8)).Children(func() {
|
|
media.Cartridge(c, "Pixel Quest cartridge", media.CartridgeOptions{Title: "Pixel Quest", Art: scene(pixel.Warn, pixel.Info, pixel.Good, 8, 4)})
|
|
media.Cartridge(c, "Night Run cartridge", media.CartridgeOptions{Title: "Night Run 2", Art: scene(pixel.Lit, pixel.Magic, pixel.Shade, 26, 3)})
|
|
})
|
|
}
|
|
|
|
func demoVinyl(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
|
media.Vinyl(c, "Record", media.VinylOptions{Playing: s.spin})
|
|
if input.Toggle(c, "PLAY", &s.spin, input.ToggleOptions{}).Changed {
|
|
log(fmt.Sprintf("record playing %v", s.spin))
|
|
}
|
|
})
|
|
}
|
|
|
|
// scene is hills under a sky, sun at x, y.
|
|
func scene(sun, sky, hill pixel.Slot, x, y int) *pixel.Sprite {
|
|
return pixel.SpriteOf(32, 18, func(i, j int) (pixel.Slot, bool) {
|
|
switch {
|
|
case (i-x)*(i-x)+(j-y)*(j-y) <= 5:
|
|
return sun, true
|
|
case float64(j) > 12+2*math.Sin(float64(i)/4):
|
|
return hill, true
|
|
}
|
|
return sky, true
|
|
})
|
|
}
|