LoginSignup
4

More than 5 years have passed since last update.

Elm: アナログ時計のsampleで現在時刻を表示させる(SVGの練習)

Posted at

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をインストールしておくこと

TimeTest.elm
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】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を追加してやることになる

ひとまずこれで、下記画像のように縁が黒くて中が薄灰色の円盤が表示される

スクリーンショット 2016-12-02 12.01.15.jpg

svgで何が表現できるかは、公式のpackageの関数一覧が参考になります

Svg

Svg.Attributes

が、Svgの細かいところ(線の太さ変えたい時とか)は、Elmのpackageで使われてる名前と

svgの関数名が若干異なってるところもあるみたいで

例えば、Elmは関数名にハイフンが使えないみたいで、SVGではstroke-widthと記述するのところが
ElmだとstrokeWidthになります


まずはsvgの公式でどんなことができるか確認して

MDN 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を引くという意味である

これで円盤に線を記述することができた

スクリーンショット 2016-12-02 12.11.46.jpg

針に現在時刻を反映させる

そもそも現在時刻はどのように取得したらいいのか?

下記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のところで、今コード時計の針の角度を得るために

  1. modelはUNIX時間のことである
  2. UNIX時間から現在の秒数にするには、Date.fromTime関数でmodelから現在日時に変換する
  3. さらに、Date.second関数で現在日時から秒だけを取り出す(変換する)
  4. 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を改変して遊んで見るのがいいかと思います

(時間の取得方法はわかったので、デジタル時計とかも作れそう)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4