Highlight root note of each chord

Refactor the Piano component to highlight the root note of each chord. If this
makes things too easy, I can support this as a preference.

Also:
- Reduced the number of keys that the piano displays and increased the key
  thickness to reclaim the space
- Preferred using Tailwind selectors instead of inline styling where applicable
- Call List.reverse on the keys to ensure that the top-most note is a lower note
  than the bottom-most note

TODO:
- Support showing only the name of the chord and not just the notes that
  comprise that chord
- Rewrite the function that generates the chords for a given range of notes
- Consider supporting a dark mode
This commit is contained in:
William Carroll 2020-04-19 00:21:37 +01:00
parent c6132ab1d6
commit 11b140b6ae
2 changed files with 70 additions and 37 deletions

View file

@ -6,6 +6,7 @@ import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import List.Extra import List.Extra
import Theory import Theory
import UI
{-| On mobile phones, the keyboard displays vertically. {-| On mobile phones, the keyboard displays vertically.
@ -17,17 +18,18 @@ type Direction
type alias KeyMarkup a = type alias KeyMarkup a =
{ offset : Int { offset : Int
, direction : Direction
, isHighlit : Bool , isHighlit : Bool
, note : Theory.Note , note : Theory.Note
, direction : Direction , isRootNote : Bool
} }
-> Html a -> Html a
type alias Props = type alias Props =
{ highlight : List Theory.Note { chord : Maybe Theory.Chord
, start : Theory.Note , firstNote : Theory.Note
, end : Theory.Note , lastNote : Theory.Note
} }
@ -44,8 +46,6 @@ naturalWidth : Direction -> Int
naturalWidth direction = naturalWidth direction =
case direction of case direction of
Vertical -> Vertical ->
-- Right now, I'm designing this specifically for my Google Pixel 4
-- phone, which has a screen width of 1080px.
1080 1080
Horizontal -> Horizontal ->
@ -58,10 +58,7 @@ naturalHeight : Direction -> Int
naturalHeight direction = naturalHeight direction =
case direction of case direction of
Vertical -> Vertical ->
-- Right now, I'm designing this specifically for my Google Pixel 4 130
-- phone, which has a screen height of 2280px. 2280 / 21
-- (i.e. no. natural keys) ~= 108
108
Horizontal -> Horizontal ->
250 250
@ -94,17 +91,28 @@ accidentalHeight direction =
{-| Return the markup for either a white or a black key. {-| Return the markup for either a white or a black key.
-} -}
pianoKey : KeyMarkup a pianoKey : KeyMarkup a
pianoKey { offset, isHighlit, note, direction } = pianoKey { offset, isHighlit, note, direction, isRootNote } =
let let
{ natColor, accColor, hiColor, rootColor } =
{ natColor = "bg-white"
, accColor = "bg-black"
, hiColor = "bg-red-400"
, rootColor = "bg-red-600"
}
sharedClasses = sharedClasses =
[ "box-border" ] [ "box-border"
, "absolute"
, "border"
, "border-black"
]
{ keyWidth, keyHeight, keyColor, offsetEdge, extraClasses } = { keyWidth, keyHeight, keyColor, offsetEdge, extraClasses } =
case ( Theory.keyClass note, direction ) of case ( Theory.keyClass note, direction ) of
( Theory.Natural, Vertical ) -> ( Theory.Natural, Vertical ) ->
{ keyWidth = naturalWidth Vertical { keyWidth = naturalWidth Vertical
, keyHeight = naturalHeight Vertical , keyHeight = naturalHeight Vertical
, keyColor = "white" , keyColor = natColor
, offsetEdge = "top" , offsetEdge = "top"
, extraClasses = [] , extraClasses = []
} }
@ -112,7 +120,7 @@ pianoKey { offset, isHighlit, note, direction } =
( Theory.Natural, Horizontal ) -> ( Theory.Natural, Horizontal ) ->
{ keyWidth = naturalWidth Horizontal { keyWidth = naturalWidth Horizontal
, keyHeight = naturalHeight Horizontal , keyHeight = naturalHeight Horizontal
, keyColor = "white" , keyColor = natColor
, offsetEdge = "left" , offsetEdge = "left"
, extraClasses = [] , extraClasses = []
} }
@ -120,7 +128,7 @@ pianoKey { offset, isHighlit, note, direction } =
( Theory.Accidental, Vertical ) -> ( Theory.Accidental, Vertical ) ->
{ keyWidth = accidentalWidth Vertical { keyWidth = accidentalWidth Vertical
, keyHeight = accidentalHeight Vertical , keyHeight = accidentalHeight Vertical
, keyColor = "black" , keyColor = accColor
, offsetEdge = "top" , offsetEdge = "top"
, extraClasses = [ "z-10" ] , extraClasses = [ "z-10" ]
} }
@ -128,26 +136,25 @@ pianoKey { offset, isHighlit, note, direction } =
( Theory.Accidental, Horizontal ) -> ( Theory.Accidental, Horizontal ) ->
{ keyWidth = accidentalWidth Horizontal { keyWidth = accidentalWidth Horizontal
, keyHeight = accidentalHeight Horizontal , keyHeight = accidentalHeight Horizontal
, keyColor = "black" , keyColor = accColor
, offsetEdge = "left" , offsetEdge = "left"
, extraClasses = [ "z-10" ] , extraClasses = [ "z-10" ]
} }
in in
div div
[ style "background-color" [ class
(if isHighlit then (case ( isHighlit, isRootNote ) of
"red" ( False, _ ) ->
keyColor
else ( True, True ) ->
keyColor rootColor
( True, False ) ->
hiColor
) )
, style "border-top" "1px solid black"
, style "border-bottom" "1px solid black"
, style "border-left" "1px solid black"
, style "border-right" "1px solid black"
, style "width" (pixelate keyWidth) , style "width" (pixelate keyWidth)
, style "height" (pixelate keyHeight) , style "height" (pixelate keyHeight)
, style "position" "absolute"
, style offsetEdge (String.fromInt offset ++ "px") , style offsetEdge (String.fromInt offset ++ "px")
, class <| String.join " " (List.concat [ sharedClasses, extraClasses ]) , class <| String.join " " (List.concat [ sharedClasses, extraClasses ])
] ]
@ -156,11 +163,18 @@ pianoKey { offset, isHighlit, note, direction } =
{-| A section of the piano consisting of all twelve notes. {-| A section of the piano consisting of all twelve notes.
-} -}
keys : Direction -> Theory.Note -> Theory.Note -> List Theory.Note -> List (Html a) keys :
keys direction start end highlight = { direction : Direction
, start : Theory.Note
, end : Theory.Note
, highlitNotes : List Theory.Note
, rootNote : Maybe Theory.Note
}
-> List (Html a)
keys { direction, start, end, highlitNotes, rootNote } =
let let
isHighlit note = isHighlit note =
List.member note highlight List.member note highlitNotes
spacing prevOffset prev curr = spacing prevOffset prev curr =
case ( Theory.keyClass prev, Theory.keyClass curr, direction ) of case ( Theory.keyClass prev, Theory.keyClass curr, direction ) of
@ -190,6 +204,7 @@ keys direction start end highlight =
( _, _, notes ) = ( _, _, notes ) =
Theory.notesFromRange start end Theory.notesFromRange start end
|> List.reverse
|> List.foldl |> List.foldl
(\curr ( prevOffset, prev, result ) -> (\curr ( prevOffset, prev, result ) ->
case ( prevOffset, prev ) of case ( prevOffset, prev ) of
@ -198,9 +213,13 @@ keys direction start end highlight =
, Just curr , Just curr
, pianoKey , pianoKey
{ offset = 0 { offset = 0
, isHighlit = List.member curr highlight , isHighlit = List.member curr highlitNotes
, note = curr , note = curr
, direction = direction , direction = direction
, isRootNote =
rootNote
|> Maybe.map (\x -> x == curr)
|> Maybe.withDefault False
} }
:: result :: result
) )
@ -214,9 +233,13 @@ keys direction start end highlight =
, Just curr , Just curr
, pianoKey , pianoKey
{ offset = offset { offset = offset
, isHighlit = List.member curr highlight , isHighlit = List.member curr highlitNotes
, note = curr , note = curr
, direction = direction , direction = direction
, isRootNote =
rootNote
|> Maybe.map (\x -> x == curr)
|> Maybe.withDefault False
} }
:: result :: result
) )
@ -227,12 +250,22 @@ keys direction start end highlight =
) )
( Nothing, Nothing, [] ) ( Nothing, Nothing, [] )
in in
List.reverse notes notes
{-| Return the HTML that renders a piano representation. {-| Return the HTML that renders a piano representation.
-} -}
render : Props -> Html a render : Props -> Html a
render { highlight, start, end } = render { chord } =
div [ style "display" "flex" ] div [ style "display" "flex" ]
(keys Vertical start end highlight |> List.reverse |> List.repeat 1 |> List.concat) (keys
{ direction = Vertical
, start = Theory.G3
, end = Theory.C6
, rootNote = chord |> Maybe.map .note
, highlitNotes =
chord
|> Maybe.andThen Theory.notesForChord
|> Maybe.withDefault []
}
)

View file

@ -37,8 +37,8 @@ render model =
, isVisible = model.isPaused , isVisible = model.isPaused
} }
, Piano.render , Piano.render
{ highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault [] { chord = model.selectedChord
, start = model.firstNote , firstNote = model.firstNote
, end = model.lastNote , lastNote = model.lastNote
} }
] ]