Files
8bitMyGoUI/gallery/demos_studio.go
T
ZacharyZhang-NY 0e21870b64 Add media components 346-365; input.SlideBase, guarded Pad
An audio player with transport, scrubber and volume; an equalizer and
a visualizer; a playlist; a cassette, cartridge and record that turn
while they play and hold where they stop; piano keys, a step sequencer
and a tracker you can type into; a waveform, a 16:9 video frame, an
image compare, a gallery, a filmstrip, a record button and an FM tuner.

input exports SlideBase, the undrawn rail its sliders share; the
gallery treats exported Bases as building blocks, not components.
pixel's micro font gains '#' for notes like C#4. pixel.Pad and the
slide rail read input only while their base is enabled, so Inert or a
fresh Disabled stops a drag or a key already sent. AGENTS records why
a drawn container keeps the DISABLED description: MyGo paints a
disabled subtree at half opacity.
2026-10-12 06:17:12 -04:00

207 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gallery
import (
"fmt"
"math"
"strings"
"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 studioEntries = []Entry{
{356, "PianoKeys", "钢琴键", "media", "Click, or type A–K. W E T Y U play the black keys.", demoPianoKeys},
{357, "StepSequencer", "步进音序器", "media", "Four tracks, sixteen steps. Press play and nod along.", demoStepSequencer},
{358, "Tracker", "音轨编辑", "media", "Type A–K to write a note, Delete for a rest. Beats every four rows.", demoTracker},
{359, "Waveform", "波形", "media", "Every sample a line, up or down from zero.", demoWaveform},
{360, "VideoFrame", "视频框", "media", "A 16:9 screen with play and a scrubber. The slime does the rest.", demoVideoFrame},
{361, "ImageCompare", "对比滑块", "media", "Drag the grip: sketch on the left, paint on the right.", demoImageCompare},
{362, "Gallery", "画廊", "media", "Arrows walk the thumbnails; the big picture follows.", demoGallery},
{363, "Filmstrip", "胶片条", "media", "A sprite's frames on film, sprocket holes and all.", demoFilmstrip},
{364, "RecordButton", "录制按钮", "media", "The dot blinks while it records, and the time runs.", demoRecordButton},
{365, "Tuner", "调频器", "media", "Drag the needle across FM. A few stations are on the air.", demoTuner},
}
func demoPianoKeys(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := mediaState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
for _, n := range media.PianoKeys(c, "Piano", media.PianoKeysOptions{}).Struck {
s.notes = append(s.notes, n)
log("♪ " + n)
}
played := s.notes[max(0, len(s.notes)-8):]
pixel.Text(c, "♪ "+strings.Join(played, " "), pixel.TextOptions{Slot: pixel.Faint})
})
}
func demoStepSequencer(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := mediaState(c)
tracks := []string{"KICK", "SNARE", "HAT", "CLAP"}
ui.Column(c).Gap(k.D(6)).Children(func() {
if media.StepSequencer(c, "Beat", tracks, s.pattern, media.StepSequencerOptions{Playing: s.beat}).Changed {
log("pattern changed")
}
if input.Toggle(c, "PLAY", &s.beat, input.ToggleOptions{}).Changed {
log(fmt.Sprintf("beat playing %v", s.beat))
}
})
}
// melody is a pattern with a tune in it.
func melody() []media.Row {
rows := make([]media.Row, 16)
for i, n := range map[int]string{0: "C-4", 2: "E-4", 4: "G-4", 6: "E-4", 8: "F-4", 10: "A-4", 12: "G-4", 14: "C-5"} {
rows[i] = media.Row{Note: n, Instrument: 1 + i/8, Volume: 64 - i*2}
}
return rows
}
func demoTracker(c *ui.Context, log func(string)) {
s := mediaState(c)
if media.Tracker(c, "Pattern", s.rows, &s.row, media.TrackerOptions{}).Changed {
log(fmt.Sprintf("pattern edited at row %02d", s.row))
}
}
func demoWaveform(c *ui.Context, log func(string)) {
// A plucked string: a fast wiggle fading out.
samples := make([]float64, 72)
for i := range samples {
f := float64(i)
samples[i] = math.Sin(f*0.7) * math.Exp(-f/24)
}
media.Waveform(c, "Pluck", samples, media.WaveformOptions{Height: 40})
}
func demoVideoFrame(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := mediaState(c)
const length = 20 * time.Second
spin(c, &s.filmed, s.video, &s.videoAt, length)
at := s.videoAt
screen := func() {
ui.Box(c).Size(k.D(160), k.D(90)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, g.W, g.H, pixel.Info)
g.Rect(0, 72, g.W, 18, pixel.Good)
g.Dither(0, 72, g.W, 18, pixel.Good, pixel.Shade, 4)
// The slime hops right with the film's own clock.
hop := int(at / (time.Second / 4))
x := int(float64(at) / float64(length) * 128)
g.Sprite(x, 48-hop%2*4, slime, hop%2, pixel.Ink, 2)
})
}
res := media.VideoFrame(c, "Slime documentary", &s.video, &s.videoAt, length, media.VideoFrameOptions{}, screen)
if res.Changed {
log(fmt.Sprintf("video playing %v at %s", s.video, s.videoAt.Truncate(time.Second)))
}
}
func demoImageCompare(c *ui.Context, log func(string)) {
s := mediaState(c)
after := scene(pixel.Warn, pixel.Info, pixel.Good, 24, 5)
// The sketch is the painting in one faint ink.
before := pixel.SpriteOf(after.W, after.H, func(x, y int) (pixel.Slot, bool) {
if sl, _ := after.At(0, x, y, pixel.Ink); sl == pixel.Info {
return pixel.Lit, true
}
return pixel.Faint, true
})
if media.ImageCompare(c, "Sketch and painting", before, after, &s.split, media.ImageCompareOptions{Scale: 4}).Changed {
log(fmt.Sprintf("split at %.0f%%", s.split*100))
}
}
var views = []struct {
name string
pic *pixel.Sprite
}{
{"Morning", scene(pixel.Warn, pixel.AccentLit, pixel.Good, 4, 10)},
{"Noon", scene(pixel.Warn, pixel.Info, pixel.Good, 16, 3)},
{"Dusk", scene(pixel.Bad, pixel.Accent, pixel.Magic, 27, 11)},
{"Night", scene(pixel.Lit, pixel.Well, pixel.Shade, 22, 4)},
}
func demoGallery(c *ui.Context, log func(string)) {
s := mediaState(c)
pics, names := make([]*pixel.Sprite, len(views)), make([]string, len(views))
for i, v := range views {
pics[i], names[i] = v.pic, v.name
}
if media.Gallery(c, "Views", pics, names, &s.chosen, media.GalleryOptions{}).Changed {
log("showing " + names[s.chosen])
}
}
var coin = pixel.MustSprite(`
..eeee..
.eyyyye.
eyylyyye
eyylyyye
eyylyyye
eyylyyye
.eyyyye.
..eeee..
...ee...
..eyye..
..eyle..
..eyle..
..eyle..
..eyle..
..eyye..
...ee...
...ee...
...ee...
...ee...
...ee...
...ee...
...ee...
...ee...
...ee...
...ee...
..eyye..
..elye..
..elye..
..elye..
..elye..
..eyye..
...ee...
`)
func demoFilmstrip(c *ui.Context, log func(string)) {
media.Filmstrip(c, "Coin spin", coin, media.FilmstripOptions{Scale: 3})
}
func demoRecordButton(c *ui.Context, log func(string)) {
s := mediaState(c)
if media.RecordButton(c, "Record", &s.rec, media.RecordButtonOptions{}).Changed {
log(fmt.Sprintf("recording %v", s.rec))
}
}
var stations = map[float64]string{88.1: "PIXEL FM", 95.5: "CHIPTUNE RADIO", 101.1: "8-BIT HITS", 104.3: "BOSS BATTLE FM"}
func demoTuner(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := mediaState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
if media.Tuner(c, "Radio", &s.mhz, media.TunerOptions{}).Changed {
log(fmt.Sprintf("%.1f MHz", s.mhz))
}
on := "· · · static · · ·"
if name, ok := stations[s.mhz]; ok {
on = "♪ " + name
}
pixel.Text(c, on, pixel.TextOptions{Slot: pixel.Faint})
})
}