From 5e448edb21e0cf02393882d23d79528a5d167f9a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 10 Feb 2023 21:55:59 -0500 Subject: [PATCH] Added sliders and made the ADSR controllabe with them --- elements/basic/lerpslider.go | 44 ++++++++ elements/basic/slider.go | 197 +++++++++++++++++++++++++++++++++++ examples/piano/main.go | 47 +++++++-- 3 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 elements/basic/lerpslider.go create mode 100644 elements/basic/slider.go diff --git a/elements/basic/lerpslider.go b/elements/basic/lerpslider.go new file mode 100644 index 0000000..51d66a9 --- /dev/null +++ b/elements/basic/lerpslider.go @@ -0,0 +1,44 @@ +package basicElements + +// Numeric is a type constraint representing a number. +type Numeric interface { + ~float32 | ~float64 | + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +type LerpSlider[T Numeric] struct { + *Slider + min T + max T +} + +func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) { + if min > max { + temp := max + max = min + min = temp + } + element = &LerpSlider[T] { + Slider: NewSlider(0, vertical), + min: min, + max: max, + } + element.SetValue(value) + return +} + +func (element *LerpSlider[T]) SetValue (value T) { + value -= element.min + element.Slider.SetValue(float64(value) / float64(element.Range())) +} + +func (element *LerpSlider[T]) Value () (value T) { + return T ( + float64(element.Slider.Value()) * float64(element.Range())) + + element.min +} + +func (element *LerpSlider[T]) Range () T { + return element.max - element.min +} diff --git a/elements/basic/slider.go b/elements/basic/slider.go new file mode 100644 index 0000000..9782ee1 --- /dev/null +++ b/elements/basic/slider.go @@ -0,0 +1,197 @@ +package basicElements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +type Slider struct { + *core.Core + *core.FocusableCore + core core.CoreControl + focusableControl core.FocusableCoreControl + + value float64 + vertical bool + dragging bool + dragOffset int + track image.Rectangle + bar image.Rectangle + + config config.Wrapped + theme theme.Wrapped + + onSlide func () + onRelease func () +} + +func NewSlider (value float64, vertical bool) (element *Slider) { + element = &Slider { + value: value, + vertical: vertical, + } + if vertical { + element.theme.Case = theme.C("basic", "sliderVertical") + } else { + element.theme.Case = theme.C("basic", "sliderHorizontal") + } + element.Core, element.core = core.NewCore(element.draw) + element.FocusableCore, + element.focusableControl = core.NewFocusableCore(element.redo) + element.updateMinimumSize() + return +} + +func (element *Slider) HandleMouseDown (x, y int, button input.Button) { + if !element.Enabled() { return } + element.Focus() + if button == input.ButtonLeft { + element.dragging = true + element.value = element.valueFor(x, y) + if element.onSlide != nil { + element.onSlide() + } + element.redo() + } +} + +func (element *Slider) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft || !element.dragging { return } + element.dragging = false + if element.onRelease != nil { + element.onRelease() + } + element.redo() +} + +func (element *Slider) HandleMouseMove (x, y int) { + if element.dragging { + element.dragging = true + element.value = element.valueFor(x, y) + if element.onSlide != nil { + element.onSlide() + } + element.redo() + } +} + +func (element *Slider) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } + +func (element *Slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + // TODO: handle left and right arrows +} + +func (element *Slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { } + +func (element *Slider) Value () (value float64) { + return element.value +} + +func (element *Slider) SetEnabled (enabled bool) { + element.focusableControl.SetEnabled(enabled) +} + +func (element *Slider) SetValue (value float64) { + if value < 0 { value = 0 } + if value > 1 { value = 1 } + + if element.value == value { return } + + element.value = value + element.redo() +} + +func (element *Slider) OnSlide (callback func ()) { + element.onSlide = callback +} + +func (element *Slider) OnRelease (callback func ()) { + element.onRelease = callback +} + +// SetTheme sets the element's theme. +func (element *Slider) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Slider) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.redo() +} + +func (element *Slider) valueFor (x, y int) (value float64) { + if element.vertical { + value = + float64(y - element.track.Min.Y - element.bar.Dy() / 2) / + float64(element.track.Dy() - element.bar.Dy()) + } else { + value = + float64(x - element.track.Min.X - element.bar.Dx() / 2) / + float64(element.track.Dx() - element.bar.Dx()) + } + + if value < 0 { value = 0 } + if value > 1 { value = 1 } + value = 1 - value + return +} + +func (element *Slider) updateMinimumSize () { + if element.vertical { + element.core.SetMinimumSize ( + element.config.HandleWidth(), + element.config.HandleWidth() * 2) + } else { + element.core.SetMinimumSize ( + element.config.HandleWidth() * 2, + element.config.HandleWidth()) + } +} + +func (element *Slider) redo () { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + +func (element *Slider) draw () { + bounds := element.Bounds() + element.track = element.theme.Inset(theme.PatternGutter).Apply(bounds) + if element.vertical { + barSize := element.track.Dx() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dy() - barSize) * + (1 - element.value) + element.bar = element.bar.Add(image.Pt(0, int(barOffset))) + } else { + barSize := element.track.Dy() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dx() - barSize) * + element.value + element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) + } + + state := theme.PatternState { + Focused: element.Focused(), + Disabled: !element.Enabled(), + Pressed: element.dragging, + } + artist.FillRectangle ( + element, + element.theme.Pattern(theme.PatternGutter, state), + bounds) + artist.FillRectangle ( + element, + element.theme.Pattern(theme.PatternHandle, state), + element.bar) +} diff --git a/examples/piano/main.go b/examples/piano/main.go index cab4024..620ae6f 100644 --- a/examples/piano/main.go +++ b/examples/piano/main.go @@ -17,6 +17,12 @@ const bufferSize = 256 var tuning = music.EqualTemparment { A4: 440 } var waveform = 0 var playing = map[music.Note] *toneStreamer { } +var adsr = ADSR { + Attack: 5 * time.Millisecond, + Decay: 400 * time.Millisecond, + Sustain: 0.7, + Release: 500 * time.Millisecond, +} func main () { speaker.Init(sampleRate, bufferSize) @@ -27,11 +33,9 @@ func run () { window, _ := tomo.NewWindow(2, 2) window.SetTitle("Piano") container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) - window.Adopt(container) controlBar := basicElements.NewContainer(basicLayouts.Horizontal { true, false }) label := basicElements.NewLabel("Play a song!", false) - controlBar.Adopt(label, true) waveformButton := basicElements.NewButton("Sine") waveformButton.OnClick (func () { waveform = (waveform + 1) % 5 @@ -43,15 +47,41 @@ func run () { case 4: waveformButton.SetText("Supersaw") } }) - controlBar.Adopt(waveformButton, false) - container.Adopt(controlBar, false) + + attackSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true) + decaySlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true) + sustainSlider := basicElements.NewSlider(adsr.Sustain, true) + releaseSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true) + + attackSlider.OnRelease (func () { + adsr.Attack = attackSlider.Value() + }) + decaySlider.OnRelease (func () { + adsr.Decay = decaySlider.Value() + }) + sustainSlider.OnRelease (func () { + adsr.Sustain = sustainSlider.Value() + }) + releaseSlider.OnRelease (func () { + adsr.Release = releaseSlider.Value() + }) piano := fun.NewPiano(2, 5) - container.Adopt(piano, true) piano.OnPress(playNote) piano.OnRelease(stopNote) piano.Focus() + window.Adopt(container) + controlBar.Adopt(label, true) + controlBar.Adopt(waveformButton, false) + controlBar.Adopt(basicElements.NewSpacer(true), false) + controlBar.Adopt(attackSlider, false) + controlBar.Adopt(decaySlider, false) + controlBar.Adopt(sustainSlider, false) + controlBar.Adopt(releaseSlider, false) + container.Adopt(controlBar, true) + container.Adopt(piano, false) + window.OnClose(tomo.Stop) window.Show() } @@ -71,12 +101,7 @@ func playNote (note music.Note) { int(tuning.Tune(note)), waveform, 0.3, - ADSR { - Attack: 100 * time.Millisecond, - Decay: 400 * time.Millisecond, - Sustain: 0.7, - Release: 500 * time.Millisecond, - }) + adsr) stopNote(note) speaker.Lock()