Elm Advent Calendar 2016 7日目の記事です
Elmのexample/timeを改変
Elmのexample codeにtimeがあるが
examples/time
これは実際の時間を表示しているわけではなくて1秒きざみに秒針が動いてるだけみたい
ので、練習がてら実際の時間を表示させてみる(長針、短針、秒針を作る)のと
どのような流れで作ったか(無理やり画面に表示してデバッグなど)、自分のような初学者の助けになればと思って記載します
作ったもの
ElmAnalogClock
環境と最終的なコード
Elmは2016年11月に出た最新版の0.18で試しています
今回作った最終的なコードは以下になります
(githubにも置いておきます)
elm-lang/svg
が必要になるのでpackageをインストールしておくこと
module TimeTest exposing (..)
import Html exposing (..)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Time exposing (..)
import Date exposing (..)
import Task
main =
Html.program
{ view = view
, init = init
, subscriptions = subscriptions
, update = update
}
-- MODEL
type alias Model =
Time
init : ( Model, Cmd Msg )
init =
( 0, now )
-- UPDATE
type Msg
= SetTime Time
update : Msg -> Model -> ( Model, Cmd Msg )
update (SetTime time) _ =
( time, Cmd.none )
-- Elm公式サイトのtimeのExampleの書き方に従うなら下記でもよい
-- update msg model =
-- case msg of
-- SetTime newTime ->
-- (newTime, Cmd.none)
now : Cmd Msg
now =
Task.perform SetTime Time.now
-- SUBSCRIPTION
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every Time.second SetTime
-- VIEW
view : Model -> Html Msg
view model =
let
secondNow =
Date.second <| Date.fromTime <| model
minuteNow =
Date.minute <| Date.fromTime <| model
hourNow =
(Date.hour <| Date.fromTime <| model) % 12
angleSecond =
toFloat <| 6 * secondNow
angleMinute =
toFloat <| 6 * minuteNow
angleHour =
-- 下記は(toFloat <| 30 * hourNow) + (angleMinute / 12) と同じ意味
(+) (angleMinute / 12) <| (toFloat <| 30 * hourNow)
handSecondX =
toString (50 + 38 * sin (angleSecond * pi / 180))
handSecondY =
toString (50 - 38 * cos (angleSecond * pi / 180))
handMinuteX =
toString (50 + 32 * sin (angleMinute * pi / 180))
handMinuteY =
toString (50 - 32 * cos (angleMinute * pi / 180))
handHourX =
toString (50 + 25 * sin (angleHour * pi / 180))
handHourY =
toString (50 - 25 * cos (angleHour * pi / 180))
in
div []
[ svg [ viewBox "0 0 100 100", width "300px" ]
[ circle [ cx "50", cy "50", r "45", fill "#2a2a2a" ] []
, circle [ cx "50", cy "50", r "40", fill "#f6f6f6" ] []
, line [ x1 "50", y1 "50", x2 handSecondX, y2 handSecondY, stroke "#2a2a2a", strokeWidth "0.5" ] []
, line [ x1 "50", y1 "50", x2 handMinuteX, y2 handMinuteY, stroke "#2a2a2a", strokeWidth "1" ] []
, line [ x1 "50", y1 "50", x2 handHourX, y2 handHourY, stroke "#2a2a2a", strokeWidth "2" ] []
]
-- 以下は時間のデバッグ用に表示
, div [] [ Html.text ("model(UNIX time): " ++ (toString model)) ]
, div [] [ Html.text ("Date.fromTime model: " ++ (toString (Date.fromTime model))) ]
]
Javascriptではどうやって作るか
JSだとCanvasを使って作れるみたいで
下記あたりが参考になりそう
アナログ時計をHTML5のCanvasとJavaScriptで作ってみたAdd Star
まずは時計の下地となる円盤を作ってみる
時計の円盤を作るだけなら、viewで表示してやるだけでよい
ただし、今回のように描画してやる場合はSVGが便利なので
導入するためにelm package install
しておく
$ elm package install elm-lang/svg -y
で、実際円盤を描画するには下記のようにする
import Svg exposing (svg, circle)
import Svg.Attributes exposing (viewBox, width, cx, cy, r, fill)
main =
view
view =
svg [viewBox "0 0 100 100", width "300px"]
-- 後に設定した円が上書きされるので、内側の円(#f6f6f6)は外側(#2a2a2a)より後に描画する
[ circle[cx "50", cy "50", r "45", fill "#2a2a2a"][]
, circle[cx "50", cy "50", r "40", fill "#f6f6f6"][]
]
import Svg exposing (svg, circle)
はviewでsvg, 円を描く circle を使うのに必要になり
import Svg.Attributes exposing (viewBox, width, cx, cy, r, fill)
はviewBoxなどパラメータに必要
例えば、後述する針を描きたい時はline
を使うのでSvgをexposingする時にline
を追加してやることになるし
線の太さ(strokeWidth)を変える場合は、Svg.Attributesをexposingする時にstrokeWidth
を追加してやることになる
ひとまずこれで、下記画像のように縁が黒くて中が薄灰色の円盤が表示される
svgで何が表現できるかは、公式のpackageの関数一覧が参考になります
が、Svgの細かいところ(線の太さ変えたい時とか)は、Elmのpackageで使われてる名前と
svgの関数名が若干異なってるところもあるみたいで
例えば、Elmは関数名にハイフンが使えないみたいで、SVGではstroke-width
と記述するのところが
ElmだとstrokeWidth
になります
まずはsvgの公式でどんなことができるか確認して
同じ関数はどれかElmのSVGのほうで確認するといいかと思います
またこれ以降この記事では、exposingする時に記述量が多くなってしまうのですべてexposing (..)
にしてしまいます
(今回は名前の衝突が無さそうなのでこれでいける)
針を追加する
次に、針を追加してみる
viewの部分に以下のようにlineに適当なパラメータを追加してやればよい
import Svg exposing (..)
import Svg.Attributes exposing (..)
main =
view
view =
svg [viewBox "0 0 100 100", width "300px"]
-- 後に設定した円が上書きされるので、内側の円(#f6f6f6)は(#2a2a2a)より後に描画する
[ circle[cx "50", cy "50", r "45", fill "#2a2a2a"][]
, circle[cx "50", cy "50", r "40", fill "#f6f6f6"][]
-- 針となるlineを追加
, line [ x1 "50", y1 "50", x2 "40", y2 "15", stroke "#2a2a2a", strokeWidth "0.5" ] []
]
これはsvgのことになるので詳細は省くが
**座標(x1, x2)と(x2, y2)を結ぶ線を、線の色"#2a2a2a", 線の太さ"0.5"**でlineを引くという意味である
これで円盤に線を記述することができた
針に現在時刻を反映させる
そもそも現在時刻はどのように取得したらいいのか?
下記stack overflowを参考にした
How do I get the current date in elm?
とりあえず現時刻のUNIX時間を取得するコードは下記になる
UNIX時間については -> wikipedia
import Html exposing (..)
import Time exposing (Time)
import Task
main : Program Never (Maybe Time) Msg
main =
Html.program
{ init = ( Nothing, now )
, view = view
, subscriptions = always Sub.none
, update = update
}
-- MODEL
type alias Model =
Maybe Time
type Msg =
SetTime (Maybe Time)
-- UPDATE
update : Msg -> Model -> (Model, Cmd Msg)
update (SetTime time) _ =
(time, Cmd.none)
-- VIEW
view : Model -> Html Msg
view model =
div [] [ text <| "(Optional) time at program launch was " ++ toString model ]
now : Cmd Msg
now =
Task.perform (Just >> SetTime) Time.now
これで、プログラムにアクセスした時間のUNIXtimeが
(Optional) time at program launch was Just 1480648806651
みたいな形で表示されるはず
上記例だとMaybeで取得されるので単純な時間を表示させるだけなら下記のようになる
import Html exposing (..)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Time exposing (..)
import Date exposing (..)
import Task
main =
Html.program
{ view = view
, init = init
, subscriptions = always Sub.none
, update = update
}
-- MODEL
type alias Model =
Time
init =
(0, now)
-- UPDATE
type Msg =
SetTime Time
update : Msg -> Model -> (Model, Cmd Msg)
update (SetTime time) _ =
(time, Cmd.none)
now : Cmd Msg
now =
Task.perform SetTime Time.now
-- VIEW
view : Model -> Html Msg
view model =
div [] [Html.text ("model(UNIX time): " ++ (toString model))]
このあたり、自分もまだよくわかっていないのだが
TimeやDateを取得する場合ElmはTaskを使って非同期処理をするため、Taskが必要となり
上記のようなコードになるみたい(もっと簡単にできるのかはすみませんわからなかったです)
ひとまず、これで時間を取得することはできるようになったので
次に針に時間を反映させてみる
秒針を表示させるプログラム
下記コードは、現在時刻から秒針だけを表示するコードである
import Html exposing (..)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Time exposing (..)
import Date exposing (..)
import Task
main =
Html.program
{ view = view
, init = init
, subscriptions = always Sub.none
, update = update
}
-- MODEL
type alias Model =
Time
init =
(0, now)
-- UPDATE
type Msg =
SetTime Time
update : Msg -> Model -> (Model, Cmd Msg)
update (SetTime time) _ =
(time, Cmd.none)
now : Cmd Msg
now =
Task.perform SetTime Time.now
-- VIEW
view : Model -> Html Msg
view model =
let
-- 現在の秒を取得
secondNow =
Date.second <| Date.fromTime <| model
-- 現在の秒から、アナログ時計にするために角度を出す
angleSecond =
toFloat <| 6 * secondNow
handSecondX =
toString (50 + 35 * sin (angleSecond * pi / 180))
handSecondY =
toString (50 - 35 * cos (angleSecond * pi / 180))
in
div []
[ svg [ viewBox "0 0 100 100", width "300px" ]
[ circle [ cx "50", cy "50", r "45", fill "#2a2a2a" ] []
, circle [ cx "50", cy "50", r "40", fill "#f6f6f6" ] []
, line [x1 "50", y1 "50", x2 handSecondX, y2 handSecondY, stroke "#2a2a2a"][]
]
]
注目すべきはviewのところで、今コード時計の針の角度を得るために
- modelはUNIX時間のことである
- UNIX時間から現在の秒数にするには、
Date.fromTime
関数でmodelから現在日時に変換する - さらに、
Date.second
関数で現在日時から秒だけを取り出す(変換する) - 1秒間に6度角度が変化するので、現在の秒数に6をかけてさらにFloat型にしたものが、秒針の角度になる
としている。
得られた角度を、sinとcosの関数を使ってやれば時計の針が表現できて上にようなコードになる
(秒針の角度からアナログ時計の動きを作る方法はjavascriptの話になるのでjavascriptでどうやってアナログ時計を表現するかのサンプルを参考にされてください)
デバッグのためにmodelなどを画面に出力させてやる
このあたりで、「modelって何が入ってるんだろ」とか「関数使ったらどんな出力結果になるのか」というのが
想像しにくかったので、自分はview
の部分をいじって以下のようにデバッグしてあげてます
(デバッグしてやるために、svgをdivで囲ってあげてる)
-- VIEW
view : Model -> Html Msg
view model =
let
-- 現在の秒を取得
secondNow =
Date.second <| Date.fromTime <| model
-- 現在の秒から、アナログ時計にするために角度を出す
angleSecond =
toFloat <| 6 * secondNow
handSecondX =
toString (50 + 35 * sin (angleSecond * pi / 180))
handSecondY =
toString (50 - 35 * cos (angleSecond * pi / 180))
in
div []
[ svg [ viewBox "0 0 100 100", width "300px" ]
[ circle [ cx "50", cy "50", r "45", fill "#2a2a2a" ] []
, circle [ cx "50", cy "50", r "40", fill "#f6f6f6" ] []
, line [x1 "50", y1 "50", x2 handSecondX, y2 handSecondY, stroke "#2a2a2a"][]
]
-- 下記3つのdivはデバッグ用, modelとmodelに関数を適用した時の出力を確認
, div [][Html.text (toString model)]
, div [][Html.text (toString (Date.fromTime <| model))]
, div [][Html.text (toString (Date.second <| Date.fromTime <| model))]
]
viewで何が出力されてるかとか、letで束縛されてる変数の中身とか途中の状態とか確認したい時
今のところ自分は上のような方法でやってます
長針と短針も追加する
以上を踏まえて、時間、分、秒を表示するプログラムは、viewの部分を以下のようにしてやればよい
-- VIEW
view : Model -> Html Msg
view model =
let
secondNow =
Date.second <| Date.fromTime <| model
minuteNow =
Date.minute <| Date.fromTime <| model
hourNow =
(Date.hour <| Date.fromTime <| model) % 12
angleSecond =
toFloat <| 6 * (Date.second <| Date.fromTime <| model)
angleMinute =
toFloat <| 6 * (Date.minute <| Date.fromTime <| model)
angleHour =
(toFloat <| 30 * ((Date.hour <| Date.fromTime <| model) % 12)) + angleMinute / 12
handSecondX =
toString (50 + 35 * sin (angleSecond * pi / 180))
handSecondY =
toString (50 - 35 * cos (angleSecond * pi / 180))
handMinuteX =
toString (50 + 35 * sin (angleMinute * pi / 180))
handMinuteY =
toString (50 - 35 * cos (angleMinute * pi / 180))
handHourX =
toString (50 + 25 * sin (angleHour * pi / 180))
handHourY =
toString (50 - 25 * cos (angleHour * pi / 180))
in
div []
[ svg [ viewBox "0 0 100 100", width "300px" ]
[ circle [ cx "50", cy "50", r "45", fill "#2a2a2a" ] []
, circle [ cx "50", cy "50", r "40", fill "#f6f6f6" ] []
, line [x1 "50", y1 "50", x2 handSecondX, y2 handSecondY, stroke "#2a2a2a", strokeWidth "0.5"][]
, line [x1 "50", y1 "50", x2 handMinuteX, y2 handMinuteY, stroke "#2a2a2a", strokeWidth "1"][]
, line [x1 "50", y1 "50", x2 handHourX, y2 handHourY, stroke "#2a2a2a", strokeWidth "2"][]
]
, div [] [Html.text ("model(UNIX time): " ++ (toString model))]
, div [] [Html.text ("Date.fromTime model: " ++ (toString (Date.fromTime model)))]
]
ちょっと冗長に書いています(本当はviewのところでsecondNowとか準備せず一気にやってよい)
ただ、このままだと逐次更新はされない(アクセスした時の時間だけ表示される)
更新されるようにupdate
をいじる
時計がアップデートされるようにする
といってもアップデートは簡単で、いままでsubscription = always Sub.none
としていた部分を
以下のようにしてやればよい(mainを書き換えて subscriptionの記述を追加する)
...
main =
Html.program
{ view = view
, init = init
, subscriptions = subscriptions
, update = update
}
...
-- SUBSCRIPTION
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every Time.second SetTime
これで現在時刻で動くアナログ時計が描画できました
最終的なコードは一番上に書いてある通りです
Elmは色々いじってみると楽しそうな言語なので
まずはsampleを改変して遊んで見るのがいいかと思います
(時間の取得方法はわかったので、デジタル時計とかも作れそう)