Media: components 341-360

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:33:40 -04:00
1 parent 6f0c02d339
commit 85575ea2e2
111 files changed
+6831 -1

No files matched your search

+116 -1
View File
@@ -1,3 +1,118 @@
package main
var mediaPages = []page{}
import (
"fmt"
"image"
"image/color"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
const mediaCategory = "Media"
var mediaPages = []page{
{ID: "341", Name: "ImageCropper", ZhName: "图片裁剪", Category: mediaCategory, Demo: imageCropperDemo, Code: imageCropperCode,
Keys: "方向键移动裁剪框;Shift+方向键缩放;R / Shift+R 向右 / 向左旋转。"},
{ID: "342", Name: "ImageAnnotator", ZhName: "图片标注", Category: mediaCategory, Demo: imageAnnotatorDemo, Code: imageAnnotatorCode,
Keys: "[ / ] 切换选中标注;方向键移动;Delete 删除;Escape 取消选择。"},
{ID: "343", Name: "ImageThumbnail", ZhName: "缩略图", Category: mediaCategory, Demo: imageThumbnailDemo, Code: imageThumbnailCode,
Keys: "Tab 聚焦;Enter / Space 激活。"},
{ID: "344", Name: "Lightbox", ZhName: "灯箱", Category: mediaCategory, Demo: lightboxDemo, Code: lightboxCode,
Keys: "← / → 切换;Home / End 首尾;Escape 关闭并恢复焦点。"},
{ID: "345", Name: "VideoPlayer", ZhName: "视频播放器", Category: mediaCategory, Demo: videoPlayerDemo, Code: videoPlayerCode,
Keys: "画面聚焦时 Space / K 播放暂停,← / → 跳转 5 秒,F 全屏,C 字幕。"},
{ID: "346", Name: "VideoScrubber", ZhName: "视频时间轴", Category: mediaCategory, Demo: videoScrubberDemo, Code: videoScrubberCode,
Keys: "← / → 跳转一个步长;Home / End 开头结尾。"},
{ID: "347", Name: "VideoThumbnailStrip", ZhName: "缩略帧条", Category: mediaCategory, Demo: videoStripDemo, Code: videoStripCode,
Keys: "Tab 聚焦帧条;← / → 上下帧;Home / End 首尾帧。"},
{ID: "348", Name: "AudioPlayer", ZhName: "音频播放器", Category: mediaCategory, Demo: audioPlayerDemo, Code: audioPlayerCode,
Keys: "Space 播放或暂停;进度条聚焦时 ← / → 跳转 5 秒。"},
{ID: "349", Name: "AudioWaveform", ZhName: "音频波形", Category: mediaCategory, Demo: audioWaveformDemo, Code: audioWaveformCode,
Keys: "← / → 跳转 5 秒;Shift+← / → 调整选区;Escape 清除选区。"},
{ID: "350", Name: "AudioSpectrum", ZhName: "音频频谱", Category: mediaCategory, Demo: audioSpectrumDemo, Code: audioSpectrumCode,
Keys: "Tab 聚焦;← / → 逐个读出频带;Home / End 首尾。"},
{ID: "351", Name: "VolumeControl", ZhName: "音量控制", Category: mediaCategory, Demo: volumeDemo, Code: volumeCode,
Keys: "← / → 音量 ±5%;Home / End;M 静音。"},
{ID: "352", Name: "PlaybackSpeedControl", ZhName: "倍速选择", Category: mediaCategory, Demo: speedDemo, Code: speedCode,
Keys: "Enter / Space 打开;↑ / ↓ 移动;Enter 确认;Escape 关闭。"},
{ID: "353", Name: "MediaControls", ZhName: "媒体控制", Category: mediaCategory, Demo: mediaControlsDemo, Code: mediaControlsCode,
Keys: "Tab 进入;← / → 在按钮间移动;Enter / Space 按下。"},
{ID: "354", Name: "Playlist", ZhName: "播放列表", Category: mediaCategory, Demo: playlistDemo, Code: playlistCode,
Keys: "↑ / ↓ 选择;Enter 播放;Alt+↑ / ↓ 移动;Delete 移除;可拖动排序。"},
{ID: "355", Name: "SubtitleEditor", ZhName: "字幕编辑器", Category: mediaCategory, Demo: subtitleDemo, Code: subtitleCode,
Keys: "Tab 在一行的开始、结束、文本、删除间移动;时间格式 m:ss.mmm。"},
{ID: "356", Name: "ScreenRecorderControls", ZhName: "录屏控制", Category: mediaCategory, Demo: recorderDemo, Code: recorderCode,
Keys: "Tab 在按钮间移动;Enter / Space 按下。"},
{ID: "357", Name: "CameraPreview", ZhName: "摄像头预览", Category: mediaCategory, Demo: cameraDemo, Code: cameraCode,
Keys: "Tab 聚焦预览;M 切换镜像。"},
{ID: "358", Name: "MicLevelMeter", ZhName: "麦克风电平", Category: mediaCategory, Demo: micDemo, Code: micCode,
Keys: "不获取焦点;读数随描述朗读。"},
{ID: "359", Name: "DeviceSelector", ZhName: "设备选择", Category: mediaCategory, Demo: deviceDemo, Code: deviceCode,
Keys: "Tab 在选择框间移动;Enter 打开;输入字母跳转。"},
{ID: "360", Name: "ImageCompare", ZhName: "前后对比", Category: mediaCategory, Demo: compareDemo, Code: compareCode,
Keys: "← / → 移动 1%;Home / End;可拖动。"},
}
// mediaSection titles a part of a demo.
func mediaSection(c *ui.Context, title string, fn func()) {
ui.Column(c).Gap(8).FillWidth().Children(func() {
ui.Text(c, title).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted)
fn()
})
}
// mediaCheck is a parameter checkbox that logs its changes.
func mediaCheck(c *ui.Context, log func(string), v *bool, label string) {
before := *v
mujica.Checkbox(c, v, label, mujica.CheckboxOptions{})
if *v != before {
log(fmt.Sprintf("%s = %v", label, *v))
}
}
// mediaPicture draws a w×h court scene in variant hues.
func mediaPicture(w, h, variant int) *image.RGBA {
walls := []color.RGBA{{36, 28, 32, 255}, {242, 235, 221, 255}, {73, 42, 54, 255}}
arch := []color.RGBA{{125, 32, 52, 255}, {173, 145, 100, 255}, {53, 94, 128, 255}}
img := image.NewRGBA(image.Rect(0, 0, w, h))
cx := float64(w) / 2
for y := range h {
for x := range w {
c := walls[variant%3]
dx, dy := (float64(x)-cx)/(float64(w)*0.28), (float64(y)-float64(h)*0.55)/(float64(h)*0.5)
if dy > 0 && math.Abs(dx) < 1 || dx*dx+dy*dy < 1 {
c = arch[variant%3]
}
if x%(w/6) < w/40 || y > h*7/8 {
c = color.RGBA{189, 161, 114, 255}
}
if math.Abs(float64(x)-cx)/float64(w)+math.Abs(float64(y)-float64(h)*0.45)/float64(h) < 0.06 {
c = color.RGBA{251, 247, 239, 255}
}
img.Set(x, y, c)
}
}
return img
}
var (
mediaPhotos = []*image.RGBA{mediaPicture(480, 270, 0), mediaPicture(480, 270, 1), mediaPicture(360, 360, 2)}
mediaBitmaps = func() []*ui.Bitmap {
out := make([]*ui.Bitmap, len(mediaPhotos))
for i, p := range mediaPhotos {
out[i] = ui.NewBitmap(p)
}
return out
}()
)
// mediaClockLabel writes d as m:ss for event logs.
func mediaClockLabel(d time.Duration) string {
s := int(d / time.Second)
return fmt.Sprintf("%d:%02d", s/60, s%60)
}
+191
View File
@@ -0,0 +1,191 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// mediaClockState simulates the caller's playback: position advances while playing.
type mediaClockState struct {
pos, dur time.Duration
playing bool
rate float64
last time.Time
}
// tick advances the position; frames are asked only while playing.
func (m *mediaClockState) tick(c *ui.Context) {
now := c.Now()
if m.playing {
if !m.last.IsZero() {
m.pos += time.Duration(float64(now.Sub(m.last)) * m.rate)
}
if m.pos >= m.dur {
m.pos, m.playing = m.dur, false
} else {
c.AnimationFrame()
}
}
m.last = now
}
func videoPlayerDemo(c *ui.Context, log func(string)) {
type state struct {
clock mediaClockState
subs, full, pip, noFrame bool
disabled bool
}
s := ui.Local(c.Root(), "345", func() state {
return state{clock: mediaClockState{dur: 3 * time.Minute, rate: 1}, subs: true}
})
s.clock.tick(c)
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.noFrame, "No frame")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
cues := []string{"The curtain rises.", "Welcome to the masquerade.", "Ave Mujica."}
var frame ui.ImageSource = mediaBitmaps[int(s.clock.pos/(20*time.Second))%3]
if s.noFrame {
frame = nil
}
st := mujica.VideoState{Frame: frame, Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing,
Rate: s.clock.rate, Buffered: []mujica.MediaRange{{Start: 0, End: s.clock.pos + 40*time.Second}},
Subtitle: cues[int(s.clock.pos/(10*time.Second))%3], SubtitlesOn: s.subs, Fullscreen: s.full, PiP: s.pip}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(640).Children(func() {
mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{
OnPlay: func() { s.clock.playing = true; log("play") },
OnPause: func() { s.clock.playing = false; log("pause") },
OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) },
OnRate: func(r float64) { s.clock.rate = r; log(fmt.Sprintf("rate %v×", r)) },
OnSubtitles: func(on bool) { s.subs = on; log(fmt.Sprintf("subtitles %v", on)) },
OnFullscreen: func(on bool) { s.full = on; log(fmt.Sprintf("full screen requested %v", on)) },
OnPiP: func(on bool) { s.pip = on; log(fmt.Sprintf("picture in picture requested %v", on)) },
Disabled: s.disabled,
})
})
})
}
func videoScrubberDemo(c *ui.Context, log func(string)) {
type state struct {
pos time.Duration
preview, disabled bool
}
s := ui.Local(c.Root(), "346", func() state { return state{pos: 40 * time.Second, preview: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.preview, "Preview slot")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
opts := mujica.VideoScrubberOptions{Duration: 3 * time.Minute, Disabled: s.disabled,
Buffered: []mujica.MediaRange{{Start: 0, End: 90 * time.Second}, {Start: 120 * time.Second, End: 140 * time.Second}}}
if s.preview {
opts.Preview = func(at time.Duration) {
ui.Image(c, mediaBitmaps[int(at/(time.Minute))%3]).Fit(ui.Cover).Fill()
}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Padding(110, 0, 0, 0).Children(func() {
if mujica.VideoScrubber(c, &s.pos, opts).Changed() {
log("seek " + mediaClockLabel(s.pos))
}
})
})
}
func audioPlayerDemo(c *ui.Context, log func(string)) {
type state struct {
clock mediaClockState
disabled bool
}
s := ui.Local(c.Root(), "348", func() state { return state{clock: mediaClockState{dur: 4*time.Minute + 5*time.Second, rate: 1}} })
s.clock.tick(c)
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Children(func() {
mujica.AudioPlayer(c, mujica.AudioState{Title: "Ave Mujica — Overture", Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing},
mujica.AudioPlayerOptions{
OnPlay: func() { s.clock.playing = true; log("play") },
OnPause: func() { s.clock.playing = false; log("pause") },
OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) },
Disabled: s.disabled,
})
})
})
}
// mediaSamples is a synthetic voice-like recording, made once.
var mediaSamples = func() []float32 {
s := make([]float32, 120000)
for i := range s {
env := 0.25 + 0.7*math.Abs(math.Sin(float64(i)/9000))*math.Abs(math.Sin(float64(i)/2100))
s[i] = float32(env * math.Sin(float64(i)/7))
}
return s
}()
func audioWaveformDemo(c *ui.Context, log func(string)) {
type state struct {
pos time.Duration
sel mujica.MediaRange
status int
selecting bool
}
s := ui.Local(c.Root(), "349", func() state { return state{pos: 20 * time.Second, selecting: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
before := s.status
mujica.SegmentedControl(c, &s.status, []mujica.Segment{{Label: "Ready"}, {Label: "Loading"}, {Label: "Failed"}, {Label: "Empty"}}, mujica.SegmentedControlOptions{Label: "Status"})
if s.status != before {
log(fmt.Sprintf("status %d", s.status))
}
mediaCheck(c, log, &s.selecting, "Selectable")
})
})
opts := mujica.AudioWaveformOptions{Duration: time.Minute, Position: s.pos, OnSeek: func(d time.Duration) { s.pos = d; log("seek " + mediaClockLabel(d)) }}
if s.selecting {
opts.Selection = &s.sel
}
data := mediaSamples
switch s.status {
case 1:
opts.Status, data = mujica.DataLoading, nil
case 2:
opts.Status, opts.Error, data = mujica.DataFailed, "The recording could not be decoded.", nil
case 3:
data = nil
}
mediaSection(c, "Live", func() {
if mujica.AudioWaveform(c, data, opts).Changed() {
log(fmt.Sprintf("selection %s – %s", mediaClockLabel(s.sel.Start), mediaClockLabel(s.sel.End)))
}
})
}
func audioSpectrumDemo(c *ui.Context, log func(string)) {
type state struct{ live bool }
s := ui.Local(c.Root(), "350", func() state { return state{live: true} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.live, "Live input") })
freqs := []string{"32 Hz", "63 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz"}
bands := make([]float32, len(freqs))
t := float64(c.Now().UnixMilli()) / 1000
for i := range bands {
bands[i] = float32(0.2 + 0.6*math.Abs(math.Sin(float64(i)*0.9+t*(1+float64(i)*0.2))))
}
if s.live {
// The demo is its own analyser; the component only draws.
c.AnimationFrame()
}
mediaSection(c, "Live", func() {
mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{BandLabel: func(i int) string { return freqs[i] }})
})
}
+178
View File
@@ -0,0 +1,178 @@
package main
// Media page snippets, each its compiled Example's body.
const (
imageCropperCode = `crop := mujica.ImageCrop{} // empty: starts fitted to the image
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageCropper(c, examplePhoto, &crop, mujica.ImageCropperOptions{Aspect: 1}).Changed() {
_ = crop.Rect // in pixels of the photo turned by crop.Rotation
}
}
ui.Render(view, 400, 300, 1)`
imageAnnotatorCode = `photo := ui.NewBitmap(examplePhoto)
var notes []mujica.Annotation
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageAnnotator(c, photo, &notes, mujica.ImageAnnotatorOptions{}).Changed() {
data, _ := mujica.ExportAnnotations(notes)
_ = data // save the JSON
}
}
ui.Render(view, 400, 300, 1)`
imageThumbnailCode = `photo := ui.NewBitmap(examplePhoto)
selected := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageThumbnail(c, photo, mujica.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() {
selected = !selected
}
}
ui.Render(view, 400, 300, 1)`
lightboxCode = `images := []mujica.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}}
open, index := false, 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "View photos", mujica.ButtonOptions{}).Clicked() {
open = true
}
mujica.Lightbox(c, &open, &index, images)
}
ui.Render(view, 400, 300, 1)`
videoPlayerCode = `st := mujica.VideoState{Duration: 4 * time.Minute, Rate: 1}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{
OnPlay: func() { st.Playing = true }, // start your decoder
OnPause: func() { st.Playing = false }, // pause it
OnSeek: func(d time.Duration) { st.Position = d },
OnRate: func(r float64) { st.Rate = r },
})
}
ui.Render(view, 400, 300, 1)`
videoScrubberCode = `pos := 30 * time.Second
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VideoScrubber(c, &pos, mujica.VideoScrubberOptions{
Duration: 2 * time.Minute,
Buffered: []mujica.MediaRange{{Start: 0, End: time.Minute}},
})
}
ui.Render(view, 400, 300, 1)`
videoStripCode = `frames := []mujica.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}}
current := 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.VideoThumbnailStrip(c, &current, frames, mujica.VideoThumbnailStripOptions{}).Changed() {
_ = frames[current].At // seek the player there
}
}
ui.Render(view, 400, 300, 1)`
audioPlayerCode = `st := mujica.AudioState{Title: "Overture", Duration: 3 * time.Minute}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioPlayer(c, st, mujica.AudioPlayerOptions{
OnPlay: func() { st.Playing = true },
OnPause: func() { st.Playing = false },
OnSeek: func(d time.Duration) { st.Position = d },
})
}
ui.Render(view, 400, 300, 1)`
audioWaveformCode = `samples := make([]float32, 48000) // from your decoder, -1 to 1
pos, sel := time.Duration(0), mujica.MediaRange{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioWaveform(c, samples, mujica.AudioWaveformOptions{
Duration: time.Second, Position: pos, Selection: &sel,
OnSeek: func(d time.Duration) { pos = d },
})
}
ui.Render(view, 400, 300, 1)`
audioSpectrumCode = `bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate()
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{})
}
ui.Render(view, 400, 300, 1)`
volumeCode = `volume, muted := 0.8, false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VolumeControl(c, &volume, &muted, mujica.VolumeControlOptions{})
}
ui.Render(view, 400, 300, 1)`
speedCode = `rate := 1.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PlaybackSpeedControl(c, &rate, mujica.PlaybackSpeedControlOptions{})
}
ui.Render(view, 400, 300, 1)`
mediaControlsCode = `st := mujica.MediaControlsState{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MediaControls(c, st, mujica.MediaControlsOptions{
OnPrevious: func() {}, OnNext: func() {},
OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false },
OnLoop: func(m mujica.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on },
})
}
ui.Render(view, 400, 300, 1)`
playlistCode = `items := []mujica.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}}
current := "a"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.Playlist(c, &items, mujica.PlaylistOptions{Current: current}); r.Played != "" {
current = r.Played
}
}
ui.Render(view, 400, 300, 1)`
subtitleCode = `cues := []mujica.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SubtitleEditor(c, &cues, mujica.SubtitleEditorOptions{})
if len(mujica.CheckSubtitles(cues)) > 0 {
_ = cues // block saving until the issues are fixed
}
}
ui.Render(view, 400, 300, 1)`
recorderCode = `st := mujica.RecorderState{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ScreenRecorderControls(c, st, mujica.ScreenRecorderControlsOptions{
OnStart: func() { st.Status = mujica.RecorderRecording },
OnPause: func() { st.Status = mujica.RecorderPaused },
OnResume: func() { st.Status = mujica.RecorderRecording },
OnStop: func() { st = mujica.RecorderState{} },
})
}
ui.Render(view, 400, 300, 1)`
cameraCode = `frame := mujica.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame
mirror := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CameraPreview(c, frame, &mirror, mujica.CameraPreviewOptions{})
}
ui.Render(view, 400, 300, 1)`
micCode = `level := float32(0.4) // the input's peak, 0 to 1 of full scale
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{})
}
ui.Render(view, 400, 300, 1)`
deviceCode = `devices := mujica.MediaDevices{
Microphones: []mujica.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}},
Speakers: []mujica.MediaDevice{{ID: "out-1", Name: "Headphones"}},
}
sel := mujica.DeviceSelection{Microphone: "mic-1"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DeviceSelector(c, &sel, devices, mujica.DeviceSelectorOptions{})
}
ui.Render(view, 400, 300, 1)`
compareCode = `before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto)
split := 0.5
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ImageCompare(c, before, after, &split, mujica.ImageCompareOptions{})
}
ui.Render(view, 400, 300, 1)`
)
+225
View File
@@ -0,0 +1,225 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func volumeDemo(c *ui.Context, log func(string)) {
type state struct {
volume float64
muted, disabled bool
}
s := ui.Local(c.Root(), "351", func() state { return state{volume: 0.7} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.VolumeControl(c, &s.volume, &s.muted, mujica.VolumeControlOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("volume %.0f%%, muted %v", s.volume*100, s.muted))
}
})
}
func speedDemo(c *ui.Context, log func(string)) {
type state struct {
rate float64
disabled bool
}
s := ui.Local(c.Root(), "352", func() state { return state{rate: 1} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.PlaybackSpeedControl(c, &s.rate, mujica.PlaybackSpeedControlOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("rate %v×", s.rate))
}
})
}
func mediaControlsDemo(c *ui.Context, log func(string)) {
type state struct {
st mujica.MediaControlsState
track int
disabled bool
}
s := ui.Local(c.Root(), "353", func() state { return state{} })
tracks := []string{"Ave Mujica", "Imprisoned XII", "KiLLKiSS", "Symbol I: △"}
s.st.NoPrevious, s.st.NoNext = s.track == 0, s.track == len(tracks)-1
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Text(c, fmt.Sprintf("Now: %s (%d of %d)", tracks[s.track], s.track+1, len(tracks)))
mujica.MediaControls(c, s.st, mujica.MediaControlsOptions{
OnPrevious: func() { s.track--; log("previous") },
OnNext: func() { s.track++; log("next") },
OnPlay: func() { s.st.Playing = true; log("play") },
OnPause: func() { s.st.Playing = false; log("pause") },
OnLoop: func(m mujica.LoopMode) { s.st.Loop = m; log(fmt.Sprintf("loop mode %d", m)) },
OnShuffle: func(on bool) { s.st.Shuffle = on; log(fmt.Sprintf("shuffle %v", on)) },
Disabled: s.disabled,
})
})
}
func playlistDemo(c *ui.Context, log func(string)) {
type state struct {
items []mujica.PlaylistItem
current string
disabled bool
}
s := ui.Local(c.Root(), "354", func() state {
titles := []string{"Ave Mujica", "Imprisoned XII", "KiLLKiSS", "Symbol I: △", "Choir 'S' Choir", "Georgette Me, Georgette You"}
items := make([]mujica.PlaylistItem, len(titles))
for i, t := range titles {
items[i] = mujica.PlaylistItem{ID: fmt.Sprint("t", i), Title: t, Subtitle: fmt.Sprintf("Act %d", i+1), Duration: time.Duration(200+i*17) * time.Second}
}
return state{items: items, current: "t1"}
})
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Add track", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
id := fmt.Sprint("t", len(s.items)+int(time.Now().UnixNano()%1000000))
s.items = append(s.items, mujica.PlaylistItem{ID: id, Title: "Encore " + id, Duration: 3 * time.Minute})
log("added " + id)
}
})
})
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(520).Children(func() {
r := mujica.Playlist(c, &s.items, mujica.PlaylistOptions{Current: s.current, Disabled: s.disabled})
switch {
case r.Played != "":
s.current = r.Played
log("play " + r.Played)
case r.Removed != "":
log("removed " + r.Removed)
case r.Moved:
log("reordered")
}
})
})
}
func subtitleDemo(c *ui.Context, log func(string)) {
type state struct {
cues []mujica.SubtitleCue
disabled bool
}
s := ui.Local(c.Root(), "355", func() state {
return state{cues: []mujica.SubtitleCue{
{Start: 0, End: 2500 * time.Millisecond, Text: "The curtain rises."},
{Start: 2 * time.Second, End: 5 * time.Second, Text: "Welcome to the masquerade."},
{Start: 6 * time.Second, End: 9 * time.Second, Text: "Ave Mujica."},
}}
})
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(720).Children(func() {
if mujica.SubtitleEditor(c, &s.cues, mujica.SubtitleEditorOptions{Height: 260, Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("%d cues, %d issues", len(s.cues), len(mujica.CheckSubtitles(s.cues))))
}
})
})
}
func recorderDemo(c *ui.Context, log func(string)) {
type state struct {
st mujica.RecorderState
since time.Time
disabled bool
accumulated time.Duration
}
s := ui.Local(c.Root(), "356", func() state { return state{} })
if s.st.Status == mujica.RecorderRecording {
s.st.Elapsed = s.accumulated + c.Now().Sub(s.since)
// The clock changes each second; ask for that frame only.
c.After(time.Second - s.st.Elapsed%time.Second)
}
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
mujica.ScreenRecorderControls(c, s.st, mujica.ScreenRecorderControlsOptions{
OnStart: func() {
s.st, s.since, s.accumulated = mujica.RecorderState{Status: mujica.RecorderRecording}, c.Now(), 0
log("start")
},
OnPause: func() { s.st.Status, s.accumulated = mujica.RecorderPaused, s.st.Elapsed; log("pause") },
OnResume: func() { s.st.Status, s.since = mujica.RecorderRecording, c.Now(); log("resume") },
OnStop: func() { log("stop at " + mediaClockLabel(s.st.Elapsed)); s.st = mujica.RecorderState{} },
Disabled: s.disabled,
})
})
}
func cameraDemo(c *ui.Context, log func(string)) {
type state struct {
mirror, signal, disabled bool
}
s := ui.Local(c.Root(), "357", func() state { return state{mirror: true, signal: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.signal, "Signal")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
frame := mujica.CameraFrame{}
if s.signal {
frame = mujica.CameraFrame{Image: mediaPhotos[2], Seq: 1}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(480).Children(func() {
if mujica.CameraPreview(c, frame, &s.mirror, mujica.CameraPreviewOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("mirror %v", s.mirror))
}
})
})
}
func micDemo(c *ui.Context, log func(string)) {
type state struct{ live bool }
s := ui.Local(c.Root(), "358", func() state { return state{live: true} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.live, "Live input") })
level := float32(0.3)
if s.live {
t := float64(c.Now().UnixMilli()) / 1000
level = float32(math.Min(1, 0.15+0.8*math.Abs(math.Sin(t*1.3)*math.Sin(t*0.37))))
c.AnimationFrame()
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(420).Children(func() { mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) })
})
}
func deviceDemo(c *ui.Context, log func(string)) {
type state struct {
sel mujica.DeviceSelection
noCamera, disabled bool
}
s := ui.Local(c.Root(), "359", func() state { return state{sel: mujica.DeviceSelection{Microphone: "m1", Speaker: "s2"}} })
devices := mujica.MediaDevices{
Microphones: []mujica.MediaDevice{{ID: "m1", Name: "Built-in Microphone"}, {ID: "m2", Name: "Studio Condenser"}},
Cameras: []mujica.MediaDevice{{ID: "c1", Name: "FaceTime HD Camera"}},
Speakers: []mujica.MediaDevice{{ID: "s1", Name: "Built-in Speakers"}, {ID: "s2", Name: "Headphones"}},
}
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
before := s.noCamera
mediaCheck(c, log, &s.noCamera, "No cameras")
if s.noCamera != before {
s.sel.Camera = ""
}
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
if s.noCamera {
devices.Cameras = []mujica.MediaDevice{}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(480).Children(func() {
if mujica.DeviceSelector(c, &s.sel, devices, mujica.DeviceSelectorOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("microphone %q, camera %q, speaker %q", s.sel.Microphone, s.sel.Camera, s.sel.Speaker))
}
})
})
}
+162
View File
@@ -0,0 +1,162 @@
package main
import (
"errors"
"fmt"
"image"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func imageCropperDemo(c *ui.Context, log func(string)) {
type state struct {
crop mujica.ImageCrop
aspect int
disabled bool
}
s := ui.Local(c.Root(), "341", func() state { return state{} })
aspects := []float64{0, 1, 16.0 / 9, 4.0 / 3}
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
before := s.aspect
mujica.SegmentedControl(c, &s.aspect, []mujica.Segment{{Label: "Free"}, {Label: "1:1"}, {Label: "16:9"}, {Label: "4:3"}}, mujica.SegmentedControlOptions{Label: "Aspect"})
if s.aspect != before {
s.crop.Rect = image.Rectangle{} // refit to the new ratio
log(fmt.Sprintf("aspect = %v", aspects[s.aspect]))
}
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
mediaSection(c, "Live", func() {
if mujica.ImageCropper(c, mediaPhotos[0], &s.crop, mujica.ImageCropperOptions{Aspect: aspects[s.aspect], Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("crop %v rotated %d°", s.crop.Rect, s.crop.Rotation))
}
})
}
func imageAnnotatorDemo(c *ui.Context, log func(string)) {
type state struct {
notes []mujica.Annotation
disabled bool
}
s := ui.Local(c.Root(), "342", func() state {
return state{notes: []mujica.Annotation{{Kind: mujica.AnnotateRect, From: image.Pt(150, 60), To: image.Pt(330, 230)}}}
})
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Export JSON", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
data, err := mujica.ExportAnnotations(s.notes)
if err != nil {
log("export failed: " + err.Error())
} else {
c.WriteClipboard(string(data))
log(fmt.Sprintf("exported %d bytes to the clipboard", len(data)))
}
}
})
})
mediaSection(c, "Live", func() {
if mujica.ImageAnnotator(c, mediaBitmaps[1], &s.notes, mujica.ImageAnnotatorOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("annotations: %d", len(s.notes)))
}
})
}
func imageThumbnailDemo(c *ui.Context, log func(string)) {
type state struct {
selected int
loading, failed, disabled bool
}
s := ui.Local(c.Root(), "343", func() state { return state{} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.loading, "Loading")
mediaCheck(c, log, &s.failed, "Failed")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
mediaSection(c, "Live", func() {
ui.Row(c).Gap(12).Wrap().Children(func() {
for i, b := range mediaBitmaps {
var err error
if s.failed && i == 2 {
err = errors.New("the file could not be read")
}
o := mujica.ImageThumbnailOptions{Alt: fmt.Sprintf("Scene %d", i+1), Aspect: 4.0 / 3, Width: 120,
Selected: s.selected == i, Loading: s.loading && i == 1, Err: err, Disabled: s.disabled}
if mujica.ImageThumbnail(c, b, o).Changed() {
s.selected = i
log(fmt.Sprintf("selected scene %d", i+1))
}
}
})
})
}
func lightboxDemo(c *ui.Context, log func(string)) {
type state struct {
open bool
index int
}
s := ui.Local(c.Root(), "344", func() state { return state{} })
images := []mujica.ViewerImage{
{Source: mediaBitmaps[0], Alt: "The grand hall at dusk"},
{Source: mediaBitmaps[1], Alt: "The ivory gallery"},
{Source: mediaBitmaps[2], Alt: "The blue chapel"},
}
mediaSection(c, "Live", func() {
ui.Row(c).Gap(12).Children(func() {
for i, im := range images {
if mujica.ImageThumbnail(c, im.Source, mujica.ImageThumbnailOptions{Alt: im.Alt, Aspect: 4.0 / 3}).Changed() {
s.open, s.index = true, i
log(fmt.Sprintf("open at %d", i+1))
}
}
})
was := s.open
if mujica.Lightbox(c, &s.open, &s.index, images).Changed() {
log(fmt.Sprintf("image %d", s.index+1))
}
if was && !s.open {
log("closed")
}
})
}
func videoStripDemo(c *ui.Context, log func(string)) {
type state struct {
current int
disabled bool
}
s := ui.Local(c.Root(), "347", func() state { return state{} })
frames := make([]mujica.StripFrame, 240)
for i := range frames {
frames[i] = mujica.StripFrame{Image: mediaBitmaps[i%3], At: time.Duration(i) * 5 * time.Second}
}
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.VideoThumbnailStrip(c, &s.current, frames, mujica.VideoThumbnailStripOptions{Disabled: s.disabled}).Changed() {
log("jump to " + mediaClockLabel(frames[s.current].At))
}
})
}
func compareDemo(c *ui.Context, log func(string)) {
type state struct {
split float64
disabled bool
}
s := ui.Local(c.Root(), "360", func() state { return state{split: 0.5} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Children(func() {
if mujica.ImageCompare(c, mediaBitmaps[0], mediaBitmaps[1], &s.split, mujica.ImageCompareOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("split %.0f%%", s.split*100))
}
})
})
}