Elm

Elmでカルーセル(スライドショー)を作る

Elm前回は軽くTodoリストを作ってみたのですが、なかなか楽しいのでカルーセル(スライドショーとも。ウェブサイトのトップでイメージを次々表示させるアレ)を作ってみました。今回の新しいのは乱数と時間の扱いに伴い「Subscription(Sub)」「Command(Cmd)」が出てくるところですかね。ちなみに完成品はこちら

というわけでやっていこう

モジュール

module Main exposing (..)

import Css exposing (..)
import Html
import Html.Styled exposing (..)
import Html.Styled.Attributes exposing (..)
import Html.Styled.Events exposing (onClick)
import Random
import Time exposing (Time, second, millisecond)

elm-cssを使ってみたかったのでそれ自体と、それ用のStyledってついたHtmlのモジュールをロードしてます。あとはRandomTimeが新顔でしょうか。

ModelとかinitとかMsgとか色々

ちょっとこの辺コードが雑で申し訳ない。

-- Model

type alias FileList = List String

type State = Initializing | Resetting | Playing
type alias Model = { position : Int
                   , state : State
                   , fileList : FileList
                   , delay : Float
                   }
type ShowStatus = Vanish | Show | Other

まずモデルです。何枚目の文字を表示するかIntで表すposition、カルーセルの状態を表してそうなstatefileListは予想がつきますね。表示させる画像のファイル名リストです。で、delayは表示切り替えの時間です。State型とかShowStatus型とかはおいおい説明します。

choiceNumber : Int -> Cmd Msg
choiceNumber n = Random.generate Select (Random.int 0 (n - 1))

init : ( Model, Cmd Msg )
init =
    let
        initial =
            { position = 1
            , state = Initializing
            , fileList = ["./img/01.jpg", "./img/02.jpg", "./img/03.jpg"]
            , delay = 4
            }
    in
        ( initial, choiceNumber (List.length initial.fileList) )

ここでCmdが出てきました。choiceNumberという関数です。Cmdは副作用(呼び出す度に値が違ってそうなやつ。例えば乱数だったり、現在時刻だったり、ネットワークだったり)を必要とする時に使います。ここでは初期化の段階でchoiceNumberを呼び出し、それをSelectで包んでメッセージとして送り出します。つまり乱数で2が出たらSelect 2というメッセージが作られるわけです。

-- Message

type Msg
    = Select Int
    | Tick Time
    | Start

で、さっき説明したSelect n以外にTick TimeStartが宣言されています。これも使う時に説明しましょう。

View

Viewはもりもり書かれてますが基本はHTMLとCSSです。アニメーションのせいで多少わかりにくいCSSになってますが、そのへんはCSSを調べていただければと思います。

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ div [ css [ position relative
                    , Css.width (px 300)
                    , Css.height (px 420)
                    ]
              ] (imageList model)
        , progress model
        , div [] (radioList model)
        ]

この辺はとりあえずHTMLとおんなじです。Css.とかついてるのはそれがついてないとどのモジュールか判別できないのでついてます。progressとかimageListとかradioListとかは次から説明していきます。

progress

progress : Model -> Html Msg
progress model =  
    div [ style [("transition", (if model.state == Resetting then "0" else toString model.delay)++"s linear")]
        , css [ backgroundImage
                    (linearGradient2
                         toRight
                         (stop2 (rgba 0 0 0 0) <| pct 50)
                         (stop2 (rgba 255 30 70 100) <| pct 50) [])
              , Css.height (px 3)
              , Css.width (px 300)
              , backgroundSize2 (pct 200) auto
              , if model.state == Resetting
                then backgroundPosition2 zero zero
                else backgroundPosition2 (pct -100) zero
              ]
        ][]

progressではプログレスバーを作っています。CSSのtransitionを使って右側ににゅるにゅるとバーを伸ばしていきます。ここでのミソはmodel.state == Rsettingというところで、リセット状態ならバーを元の位置に戻します。

imageList

多分ここがめんどくさいです。やりたいことはフェードで前の絵を消して次の絵を出したい!ってとこなので一番上に一つ前の消すやつ、二番目に見せたいやつすなわちmodel.positionで指されてるやつが来ます。

showStatus : Int -> Int -> Int -> ShowStatus
showStatus n show all =
    if show == n
    then Show
    else if (show - n + all) % all == 1
         then Vanish
         else Other

これが前述のような状態を作る関数です。showは見せるべき絵(すなわちmodel.position)で、それぞれの絵が見せる状態Showなのか、消えるべき一つ前の絵Vanishなのか、それ以外Otherなのかを決定します。

imageList : Model -> List (Html Msg)
imageList model =
    let
        image (n, name) =
            let
                state = showStatus n model.position (List.length model.fileList)
            in
                div
                [ id ("image" ++ toString n)
                , style [("transition", "0.5s")]
                , css [ backgroundImage (url name)
                      , backgroundSize cover
                      , Css.width (px 300)
                      , Css.height (px 400)
                      , position absolute
                      , left zero
                      , top zero
                      , zIndex (if state == Show
                                then int 10
                                else if state == Vanish
                                     then int 20
                                     else int 0)
                      , opacity (if model.state == Initializing
                                 then int 0 else if state == Show
                                             then int 100
                                             else int 0)
                      ]
                ] []
    in
        List.map image (List.indexedMap (,) model.fileList)

で、その状態を元にz-indexopacityを決めていくわけですね。Vanishなのがz-index高いのはなんか不思議ですが、こいつはopacity0なので、transitionで決められた時間でゆっくりと消えるわけです。このあたりはElmと言うよりCSSですね……。

radioList : Model -> List (Html Msg)
radioList model =
    let
        radio n = input [ type_ "radio"
                        , name "position"
                        , Html.Styled.Attributes.checked (n == model.position)
                        , onClick ( Select n ) ] []
    in
       List.map radio ( List.range 0 ( (List.length model.fileList) - 1 ) )

こちらがラジオボタンの実装になります。クリックするとそのラジオボタンに対応したSelect nメッセージを発行します。

Update

次にUpdateです。今回はSub/Cmdも取り入れているためここでCmdを発行することができますが、今回は特に使いません。! []となっているのはCmd.noneを発行するよと言っているようなものです(本来なら[Cmd1, Cmd2, ...]のように多数のCmdを実行することができる)。

-- Update


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Tick _ ->
            { model | position = ( model.position + 1 ) % (List.length model.fileList)
            , state = Resetting } ! []
        Select n ->
            { model | position = n , state = Resetting } ! []
        Start ->
            { model | state = Playing } ! []

Tickと言うのは後述のSubscriptionから送られるメッセージです。このメッセージを受け取るとmodel.positionを1つすすめる(そして範囲内に丸める)、つまりfileListの次の絵を指すことになります。Select nはラジオボタンから番号を指定して送られるメッセージでしたね。そしてこのどちらもmodel.stateResettingにしています。

Subscription

今回の肝となりそうなところです。Subscriptionと言うのは定期購読、つまり何らかのイベントを待ち受けて何かしたいという時に使います。今回の例で言えば一定時間立った時の処理、です。

-- Subscription

subscriptions : Model -> Sub Msg
subscriptions model =
    case model.state of
        Playing ->
            Time.every ( second * model.delay ) Tick
        Resetting ->
            Time.every ( millisecond * 100 ) (\_ -> Start)
        Initializing ->
            Sub.none

modelの状態によってSubscriptionも変わっています。まずはPlayingの時。つまり何も触らず放置してる時は一定間隔でTickメッセージが送られます。そしてTickメッセージを受け取るとどうなるかというと……絵が一つ進むんでしたね。これで次々絵が変わっていきます。
次にResettingですが、これはTickにしろSelect nにしろ、絵が変わる時に一旦この状態になります。そしてStartのメッセージを発行することによってPlayingの状態に以降し、Tickが発行されるのを待ちます。(このへん実はこの実装でいいのか悩んでます。リセット付きのタイマーでもっといい感じの実装ある方はご連絡ください)

そしてあとはメイン、ですね。

-- MAIN

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view >> toUnstyled
        , update = update
        , subscriptions = subscriptions
        }

ソースコードはGithubに上げてあるのでまとめてほしい方はこちらをどうぞ(絵もついてきます!)。

次はネットワークに関係したところも扱っていきたいですね。そうなるとサーバも必要なんですが、そっちをErlangで実装したい。というかElm書くよりそっちのほうが重いんじゃないかっていうね……。