about summary refs log blame commit diff
path: root/website/sandbox/learnpianochords/src/Main.elm
blob: 1c40d0d7961a7e6802d284c6f75a1dbfeac6fd00 (plain) (tree)
1
2
3
4
5
6
7
8
9
10





                                    
           
            

                  

             
                         
         
 
 
                  
                                           

                                                        
                                                      
                                       
                                        



                             
                                 
                 


     




                 






                                            







                           
                                           
                                      
                                        
                          
               


                                  
                  
 
 

                                                      
               


           





                                                                         





                                                
 

                                          

            

                                 
                                    






                                

                                  

              
                                                                  


                      
      













                                                          

                                        
                                            
                            
                             
                     
                

                           
                        

     
 
                                










                                                                            
                  
               


                               







                                             


                          
                                 



                      
















                                                                        
                         
                                                    

                      









                                            
                                     























                                                            
















                                                                                
                                                                      




                         










                                                                                





                                                                  
                                                                      




                         
                                      
               


                                                                                   

                        
                                                                   

                     
                                                        





                                                                  
                                                     
                         



                      
















                                                                    












                                              

                             







                                                 


















                                                                            








                                                                            
                                              








                                                                        


           

                                    

               
                                                  


                                                 


                                                                

                             


                                                             
                                    
                                                                    





                                 





















                                                                
          
                                                                                        
               
                           
                           
                                         
                                       

                                                            
                             




                     






























                                                                


                          
                                                      













                                                       


                                     

                          
                                 

                                    

                                    
                                   


                           
                                                                     

                                                                     
         
 


                            
       
                                     
                                  
                                             

                
                             

          





                                        




                                                                                                           
         

 









                             

                                                                   
      





                                          
module Main exposing (main)

import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Icon
import Piano
import Random
import Random.List
import Tempo
import Theory
import Time exposing (..)
import UI


type alias Model =
    { whitelistedChords : List Theory.Chord
    , whitelistedChordTypes : List Theory.ChordType
    , whitelistedInversions : List Theory.ChordInversion
    , whitelistedPitchClasses : List Theory.PitchClass
    , whitelistedKeys : List Theory.Key
    , selectedChord : Maybe Theory.Chord
    , isPaused : Bool
    , tempo : Int
    , firstNote : Theory.Note
    , lastNote : Theory.Note
    , practiceMode : PracticeMode
    , view : View
    }


type View
    = Preferences
    | Practice


{-| Control the type of practice you'd like.
-}
type PracticeMode
    = KeyMode
    | FineTuneMode


type Msg
    = NextChord
    | NewChord Theory.Chord
    | Play
    | Pause
    | IncreaseTempo
    | DecreaseTempo
    | SetTempo String
    | ToggleInversion Theory.ChordInversion
    | ToggleChordType Theory.ChordType
    | TogglePitchClass Theory.PitchClass
    | ToggleKey Theory.Key
    | DoNothing
    | SetPracticeMode PracticeMode
    | SelectAllKeys
    | DeselectAllKeys
    | SetView View


{-| The amount by which we increase or decrease tempo.
-}
tempoStep : Int
tempoStep =
    5


{-| Return the number of milliseconds that elapse during an interval in a
`target` bpm.
-}
bpmToMilliseconds : Int -> Int
bpmToMilliseconds target =
    let
        msPerMinute =
            1000 * 60
    in
    round (toFloat msPerMinute / toFloat target)


{-| The initial state for the application.
-}
init : Model
init =
    let
        ( firstNote, lastNote ) =
            ( Theory.C3, Theory.C6 )

        inversions =
            Theory.allInversions

        chordTypes =
            Theory.allChordTypes

        pitchClasses =
            Theory.allPitchClasses

        keys =
            [ { pitchClass = Theory.C, mode = Theory.MajorMode } ]

        practiceMode =
            KeyMode
    in
    { practiceMode = practiceMode
    , whitelistedChords =
        case practiceMode of
            KeyMode ->
                keys |> List.concatMap Theory.chordsForKey

            FineTuneMode ->
                Theory.allChords
                    { start = firstNote
                    , end = lastNote
                    , inversions = inversions
                    , chordTypes = chordTypes
                    , pitchClasses = pitchClasses
                    }
    , whitelistedChordTypes = chordTypes
    , whitelistedInversions = inversions
    , whitelistedPitchClasses = pitchClasses
    , whitelistedKeys = keys
    , selectedChord = Nothing
    , isPaused = True
    , tempo = 20
    , firstNote = firstNote
    , lastNote = lastNote
    , view = Preferences
    }


subscriptions : Model -> Sub Msg
subscriptions { isPaused, tempo } =
    if isPaused then
        Sub.none

    else
        Time.every (tempo |> bpmToMilliseconds |> toFloat) (\_ -> NextChord)


{-| Now that we have state, we need a function to change the state.
-}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        DoNothing ->
            ( model, Cmd.none )

        SetPracticeMode practiceMode ->
            ( { model
                | practiceMode = practiceMode
                , isPaused = True
              }
            , Cmd.none
            )

        SetView x ->
            ( { model
                | view = x
                , isPaused = True
              }
            , Cmd.none
            )

        SelectAllKeys ->
            ( { model
                | whitelistedKeys = Theory.allKeys
                , whitelistedChords =
                    Theory.allKeys |> List.concatMap Theory.chordsForKey
              }
            , Cmd.none
            )

        DeselectAllKeys ->
            ( { model
                | whitelistedKeys = []
                , whitelistedChords = []
              }
            , Cmd.none
            )

        NewChord chord ->
            ( { model | selectedChord = Just chord }
            , Cmd.none
            )

        NextChord ->
            ( model
            , Random.generate
                (\x ->
                    case x of
                        ( Just chord, _ ) ->
                            NewChord chord

                        ( Nothing, _ ) ->
                            DoNothing
                )
                (Random.List.choose model.whitelistedChords)
            )

        Play ->
            ( { model | isPaused = False }
            , Cmd.none
            )

        Pause ->
            ( { model | isPaused = True }
            , Cmd.none
            )

        IncreaseTempo ->
            ( { model | tempo = model.tempo + tempoStep }
            , Cmd.none
            )

        DecreaseTempo ->
            ( { model | tempo = model.tempo - tempoStep }
            , Cmd.none
            )

        ToggleChordType chordType ->
            let
                chordTypes =
                    if List.member chordType model.whitelistedChordTypes then
                        List.filter ((/=) chordType) model.whitelistedChordTypes

                    else
                        chordType :: model.whitelistedChordTypes
            in
            ( { model
                | whitelistedChordTypes = chordTypes
                , whitelistedChords =
                    Theory.allChords
                        { start = model.firstNote
                        , end = model.lastNote
                        , inversions = model.whitelistedInversions
                        , chordTypes = chordTypes
                        , pitchClasses = model.whitelistedPitchClasses
                        }
              }
            , Cmd.none
            )

        ToggleInversion inversion ->
            let
                inversions =
                    if List.member inversion model.whitelistedInversions then
                        List.filter ((/=) inversion) model.whitelistedInversions

                    else
                        inversion :: model.whitelistedInversions
            in
            ( { model
                | whitelistedInversions = inversions
                , whitelistedChords =
                    Theory.allChords
                        { start = model.firstNote
                        , end = model.lastNote
                        , inversions = inversions
                        , chordTypes = model.whitelistedChordTypes
                        , pitchClasses = model.whitelistedPitchClasses
                        }
              }
            , Cmd.none
            )

        TogglePitchClass pitchClass ->
            let
                pitchClasses =
                    if List.member pitchClass model.whitelistedPitchClasses then
                        List.filter ((/=) pitchClass) model.whitelistedPitchClasses

                    else
                        pitchClass :: model.whitelistedPitchClasses
            in
            ( { model
                | whitelistedPitchClasses = pitchClasses
                , whitelistedChords =
                    Theory.allChords
                        { start = model.firstNote
                        , end = model.lastNote
                        , inversions = model.whitelistedInversions
                        , chordTypes = model.whitelistedChordTypes
                        , pitchClasses = pitchClasses
                        }
              }
            , Cmd.none
            )

        ToggleKey key ->
            let
                keys =
                    if List.member key model.whitelistedKeys then
                        List.filter ((/=) key) model.whitelistedKeys

                    else
                        key :: model.whitelistedKeys
            in
            ( { model
                | whitelistedKeys = keys
                , whitelistedChords =
                    keys |> List.concatMap Theory.chordsForKey
              }
            , Cmd.none
            )

        SetTempo tempo ->
            ( { model
                | tempo =
                    case String.toInt tempo of
                        Just x ->
                            x

                        Nothing ->
                            model.tempo
              }
            , Cmd.none
            )


playPause : Model -> Html Msg
playPause { isPaused } =
    if isPaused then
        button [ onClick Play ] [ text "Play" ]

    else
        button [ onClick Pause ] [ text "Pause" ]


chordTypeCheckboxes : List Theory.ChordType -> Html Msg
chordTypeCheckboxes chordTypes =
    ul []
        (Theory.allChordTypes
            |> List.map
                (\chordType ->
                    li []
                        [ label [] [ text (Theory.chordTypeName chordType) ]
                        , input
                            [ type_ "checkbox"
                            , onClick (ToggleChordType chordType)
                            , checked (List.member chordType chordTypes)
                            ]
                            []
                        ]
                )
        )


inversionCheckboxes : List Theory.ChordInversion -> Html Msg
inversionCheckboxes inversions =
    ul []
        (Theory.allInversions
            |> List.map
                (\inversion ->
                    li []
                        [ label [] [ text (Theory.inversionName inversion) ]
                        , input
                            [ type_ "checkbox"
                            , onClick (ToggleInversion inversion)
                            , checked (List.member inversion inversions)
                            ]
                            []
                        ]
                )
        )


selectKey :
    Model
    ->
        { relativeMajor : Theory.Key
        , relativeMinor : Theory.Key
        }
    -> Html Msg
selectKey model { relativeMajor, relativeMinor } =
    let
        active key =
            List.member key model.whitelistedKeys

        buttonLabel major minor =
            Theory.viewKey major ++ ", " ++ Theory.viewKey minor
    in
    div [ class "flex pt-0" ]
        [ UI.textToggleButton
            { label = buttonLabel relativeMajor relativeMinor
            , handleClick = ToggleKey relativeMinor
            , classes = [ "flex-1" ]
            , toggled = active relativeMinor || active relativeMajor
            }
        ]


keyCheckboxes : Model -> Html Msg
keyCheckboxes model =
    let
        majorKey pitchClass =
            { pitchClass = pitchClass, mode = Theory.MajorMode }

        minorKey pitchClass =
            { pitchClass = pitchClass, mode = Theory.MinorMode }

        circleOfFifths =
            [ ( Theory.C, Theory.A )
            , ( Theory.G, Theory.E )
            , ( Theory.D, Theory.B )
            , ( Theory.A, Theory.F_sharp )
            , ( Theory.E, Theory.C_sharp )
            , ( Theory.B, Theory.G_sharp )
            , ( Theory.F_sharp, Theory.D_sharp )
            , ( Theory.C_sharp, Theory.A_sharp )
            , ( Theory.G_sharp, Theory.F )
            , ( Theory.D_sharp, Theory.C )
            , ( Theory.A_sharp, Theory.G )
            , ( Theory.F, Theory.D )
            ]
    in
    div []
        [ h2 [ class "text-gray-500 text-center pt-10 text-5xl" ] [ text "Select keys" ]
        , ul []
            (circleOfFifths
                |> List.map
                    (\( major, minor ) ->
                        selectKey model
                            { relativeMajor = majorKey major
                            , relativeMinor = minorKey minor
                            }
                    )
            )
        ]


practiceModeButtons : Model -> Html Msg
practiceModeButtons model =
    div [ class "text-center" ]
        [ h2 [ class "py-10 text-5xl" ] [ text "Practice Mode" ]
        , div [ class "flex pb-6" ]
            [ UI.simpleButton
                { label = "Key"
                , classes = [ "flex-1", "rounded-r-none" ]
                , handleClick = SetPracticeMode KeyMode
                , color =
                    if model.practiceMode == KeyMode then
                        UI.Primary

                    else
                        UI.Secondary
                }
            , UI.simpleButton
                { label = "Fine Tune"
                , handleClick = SetPracticeMode FineTuneMode
                , classes = [ "flex-1", "rounded-l-none" ]
                , color =
                    if model.practiceMode == FineTuneMode then
                        UI.Primary

                    else
                        UI.Secondary
                }
            ]
        ]


openPreferences : Html Msg
openPreferences =
    button
        [ class "w-48 h-48 absolute left-0 top-0 z-40"
        , onClick (SetView Preferences)
        ]
        [ Icon.cog ]


closePreferences : Html Msg
closePreferences =
    button
        [ class "w-48 h-48 absolute right-0 top-0 z-10"
        , onClick (SetView Practice)
        ]
        [ Icon.close ]


preferences : Model -> Html Msg
preferences model =
    div [ class "pt-10 pb-20 px-10" ]
        [ closePreferences
        , Tempo.render
            { tempo = model.tempo
            , handleInput = SetTempo
            }
        , case model.practiceMode of
            KeyMode ->
                keyCheckboxes model

            FineTuneMode ->
                div []
                    [ inversionCheckboxes model.whitelistedInversions
                    , chordTypeCheckboxes model.whitelistedChordTypes
                    ]
        ]


practice : Model -> Html Msg
practice model =
    let
        ( handleClick, buttonText ) =
            if model.isPaused then
                ( Play, "Press to practice" )

            else
                ( Pause, "" )
    in
    div []
        [ openPreferences
        , UI.overlayButton
            { label = buttonText
            , handleClick = handleClick
            , isVisible = model.isPaused
            }
        , Piano.render
            { highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault []
            , start = model.firstNote
            , end = model.lastNote
            }
        ]


view : Model -> Html Msg
view model =
    case model.view of
        Preferences ->
            preferences model

        Practice ->
            practice model


{-| For now, I'm just dumping things onto the page to sketch ideas.
-}
main =
    Browser.element
        { init = \() -> ( init, Cmd.none )
        , subscriptions = subscriptions
        , update = update
        , view = view
        }