diff --git a/elements/fun/piano.go b/elements/fun/piano.go new file mode 100644 index 0000000..900c6a3 --- /dev/null +++ b/elements/fun/piano.go @@ -0,0 +1,244 @@ +package fun + +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" + +// Octave represents a MIDI octave. +type Octave int + +// Note returns the note at the specified scale degree in the chromatic scale. +func (octave Octave) Note (degree int) Note { + return Note(int(octave + 1) * 12 + degree) +} + +// Note represents a MIDI note. +type Note int + +// Octave returns the octave of the note +func (note Note) Octave () int { + return int(note / 12 - 1) +} + +// Degree returns the scale degree of the note in the chromatic scale. +func (note Note) Degree () int { + mod := note % 12 + if mod < 0 { mod += 12 } + return int(mod) +} + +// IsSharp returns whether or not the note is a sharp. +func (note Note) IsSharp () bool { + degree := note.Degree() + return degree == 1 || + degree == 3 || + degree == 6 || + degree == 8 || + degree == 10 +} + +const pianoKeyWidth = 18 + +type pianoKey struct { + image.Rectangle + Note +} + +type Piano struct { + *core.Core + core core.CoreControl + low, high Octave + + config config.Wrapped + theme theme.Wrapped + + flatKeys []pianoKey + sharpKeys []pianoKey + + pressed *pianoKey + + onPress func (Note) + onRelease func (Note) +} + +func NewPiano (low, high Octave) (element *Piano) { + element = &Piano { + low: low, + high: high, + } + element.theme.Case = theme.C("fun", "piano") + element.Core, element.core = core.NewCore (func () { + element.recalculate() + element.draw() + }) + element.updateMinimumSize() + return +} + +// OnPress sets a function to be called when a key is pressed. +func (element *Piano) OnPress (callback func (note Note)) { + element.onPress = callback +} + +// OnRelease sets a function to be called when a key is released. +func (element *Piano) OnRelease (callback func (note Note)) { + element.onRelease = callback +} + +func (element *Piano) HandleMouseDown (x, y int, button input.Button) { + if button != input.ButtonLeft { return } + element.pressUnderMouseCursor(image.Pt(x, y)) +} + +func (element *Piano) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft { return } + if element.onRelease != nil { + element.onRelease((*element.pressed).Note) + } + element.pressed = nil + element.redo() +} + +func (element *Piano) HandleMouseMove (x, y int) { + if element.pressed == nil { return } + element.pressUnderMouseCursor(image.Pt(x, y)) +} + +func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } + +func (element *Piano) pressUnderMouseCursor (point image.Point) { + // release previous note + if element.pressed != nil && element.onRelease != nil { + element.onRelease((*element.pressed).Note) + } + + // find out which note is being pressed + newKey := (*pianoKey)(nil) + for index, key := range element.flatKeys { + if point.In(key.Rectangle) { + newKey = &element.flatKeys[index] + break + } + } + for index, key := range element.sharpKeys { + if point.In(key.Rectangle) { + newKey = &element.sharpKeys[index] + break + } + } + if newKey == nil { return } + + if newKey != element.pressed { + // press new note + element.pressed = newKey + if element.onPress != nil { + element.onPress((*element.pressed).Note) + } + element.redo() + } +} + +// SetTheme sets the element's theme. +func (element *Piano) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.updateMinimumSize() + element.recalculate() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Piano) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.recalculate() + element.redo() +} + +func (element *Piano) updateMinimumSize () { + element.core.SetMinimumSize ( + pianoKeyWidth * 7 * element.countOctaves(), 64) +} + +func (element *Piano) countOctaves () int { + return int(element.high - element.low + 1) +} + +func (element *Piano) countFlats () int { + return element.countOctaves() * 8 +} + +func (element *Piano) countSharps () int { + return element.countOctaves() * 5 +} + +func (element *Piano) redo () { + if element.core.HasImage() { + element.draw() + element.core.DamageAll() + } +} + +func (element *Piano) recalculate () { + element.flatKeys = make([]pianoKey, element.countFlats()) + element.sharpKeys = make([]pianoKey, element.countSharps()) + + bounds := element.Bounds() + dot := bounds.Min + note := element.low.Note(0) + limit := element.high.Note(12) + flatIndex := 0 + sharpIndex := 0 + for note < limit { + if note.IsSharp() { + element.sharpKeys[sharpIndex].Rectangle = image.Rect ( + -(pianoKeyWidth * 3) / 7, 0, + (pianoKeyWidth * 3) / 7, + bounds.Dy() / 2).Add(dot) + element.sharpKeys[sharpIndex].Note = note + sharpIndex ++ + } else { + element.flatKeys[flatIndex].Rectangle = image.Rect ( + 0, 0, pianoKeyWidth, bounds.Dy()).Add(dot) + dot.X += pianoKeyWidth + element.flatKeys[flatIndex].Note = note + flatIndex ++ + } + note ++ + } +} + +func (element *Piano) draw () { + for _, key := range element.flatKeys { + element.drawFlat ( + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note) + } + for _, key := range element.sharpKeys { + element.drawSharp ( + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note) + } +} + +func (element *Piano) drawFlat (bounds image.Rectangle, pressed bool) { + state := theme.PatternState { + Pressed: pressed, + } + pattern := element.theme.Pattern(theme.PatternButton, state) + artist.FillRectangle(element, pattern, bounds) +} + +func (element *Piano) drawSharp (bounds image.Rectangle, pressed bool) { + state := theme.PatternState { + Pressed: pressed, + } + pattern := element.theme.Pattern(theme.PatternButton, state) + artist.FillRectangle(element, pattern, bounds) +} diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 86840be..0a383d1 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -15,7 +15,7 @@ func main () { func run () { window, _ := tomo.NewWindow(2, 2) - window.SetTitle("clock") + window.SetTitle("Clock") container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) window.Adopt(container) diff --git a/examples/piano/main.go b/examples/piano/main.go new file mode 100644 index 0000000..bc74a2c --- /dev/null +++ b/examples/piano/main.go @@ -0,0 +1,26 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" +import "git.tebibyte.media/sashakoshka/tomo/elements/fun" +import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(2, 2) + window.SetTitle("Piano") + container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) + window.Adopt(container) + + label := basicElements.NewLabel("Play a song!", false) + container.Adopt(label, false) + piano := fun.NewPiano(3, 5) + container.Adopt(piano, true) + + window.OnClose(tomo.Stop) + window.Show() +}