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:
parent
c6132ab1d6
commit
11b140b6ae
2 changed files with 70 additions and 37 deletions
|
@ -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, _ ) ->
|
||||||
|
|
||||||
else
|
|
||||||
keyColor
|
keyColor
|
||||||
|
|
||||||
|
( True, True ) ->
|
||||||
|
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 []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue