Day 5
Let the Tree Grow
We are still working on this content.
Stay tuned
Today we are going to learn about
The Elm Architecture
Animation frames

The Problem

We have a nice picture of a tree, but ultimately it may have been easier to just draw it using some graphics program. We promised you a growing tree, so let’s make it grow like this:

The Elm Architecture

So far our programs were very static. The value of
main
would be evaluated once, painted on the screen and stay there forever. Now we need to make our program react to the events in outside world. What events? A passage of time
If we want our tree to grow over time, we need to know how much time have passed, so we can update it’s age accordingly.
In Elm we can do it using a different type of
main
. One that ties together four functions:
1.
init
specifying what’s the initial state of the running program
2.
subscriptions
specifying what events we are expecting and what messages will be sent when they happen
3.
update
specifying how the state of the application will change when messages are received
4.
view
specifying what value should be displayed depending on the current state.
State is sometimes called model. There are also commands, but we will not use them in our program.
"Model View Update - The Elm Architecture"
“The Elm Architecture” by
Kolja Wilcke
is licenced under
CC BY 4.0
First let’s change the value of
main
and create the
view
function, like this:
Elm
1
module Main exposing (main)
2
3
import Browser
4
import Dict
5
import Element
6
import Svg exposing (Svg)
7
import Svg.Attributes
8
9
10
main =
11
Browser.element
12
{ init = init
13
, view = view
14
, update = update
15
, subscriptions = subscriptions
16
}
17
18
19
view age =
20
[ segment (age / 5000) { color = "brown", rotation = -90 } ]
21
|> Svg.svg
22
[ Svg.Attributes.height "100%"
23
, Svg.Attributes.width "100%"
24
, Svg.Attributes.style "background: none"
25
, Svg.Attributes.viewBox "-500 -500 1000 1000"
26
]
27
|> Element.html
28
|> Element.layout
29
[ Element.width Element.fill
30
, Element.height Element.fill
31
]
32 - 112
unfold
Notice that the
view
takes an argument called
age
and passes it to the first (brown) segment. The value of
age
is our state. In some programs the type of state can be very complex, but in our program it’s simply a
Float
number. It will indicate how much time in milliseconds have passed from the start of the program. Because the time is counted in milliseconds, to get correct results we need to divide it by 5000 (so the tree will grow one generation of segments per five second).
Now let’s provide the init function. It takes an argument sometimes called flags, but we will ignore it. Let’s just say that it will always receive
()
- an empty value called unit. It needs to return two values bound together in a structure called
tuple
.
Tuple is like a list, but have a fixed number of elements. Unlike the lists, elements of tuples can have different types.
The second value is a command. We don’t need any, so we just give it a
Cmd.none
, which as you can guess produces a command that does nothing.
First value is more important. This is the initial age of the tree, right after the program starts. Since no time have passed yet, we set it to 0.
That’s our init:
Elm
1
init () =
2
( 0, Cmd.none )
3
4
Time for the
update
function. It will get two arguments: an incoming message and the current state. Incoming message will always be a duration of time since previous frame, so let’s simply call it
duration
. The state is the current age of the tree, so again we can call it
age
. In return we must produce a tuple with:
1.
the new state (
age + duration
)
2.
a command (we don’t need it, so we give
Cmd.none
)
It looks like this:
Elm
1
update duration age =
2
( age + duration
3
, Cmd.none
4
)
5
6
Finally let’s subscribe to the events marking the passage of time. We can do it using
Browser.Events.onAnimationFrameDelta
. Here is how it works. Whenever the browser is ready for the next frame it will send us a message containing the number of milliseconds that have passed since previous frame.
To use it we first need to import the
Browser.Events
module. The
Browser.Events.onAnimationFrameDelta
function expects us to give it a function that will get a duration and return a message. But our message is simply a duration! We don’t need to do anything with it, just take it as it is. So we need a function that just returns whatever it gets. This function is called
identity
.
Perhaps you have noticed that we use a lot of functions. Functions are passed to functions that sometimes return functions 🤕 That’s why Elm is called a
functional programming language
.
The complete code like this:
Elm
1
module Main exposing (main)
2
3
import Browser
4
import Browser.Events
5
import Dict
6
import Element
7
import Svg exposing (Svg)
8
import Svg.Attributes
9
10
11
main =
12
Browser.element
13
{ init = init
14
, view = view
15
, update = update
16
, subscriptions = subscriptions
17
}
18
19
20
init () =
21
( 0, Cmd.none )
22
23
24
view age =
25
[ segment (age / 5000) { color = "brown", rotation = -90 } ]
26
|> Svg.svg
27
[ Svg.Attributes.height "100%"
28
, Svg.Attributes.width "100%"
29
, Svg.Attributes.style "background: none"
30
, Svg.Attributes.viewBox "-500 -500 1000 1000"
31
]
32
|> Element.html
33
|> Element.layout
34
[ Element.width Element.fill
35
, Element.height Element.fill
36
]
37
38
39
update duration age =
40
( duration
41
|> Debug.log "Duration"
42
|> (+) age
43
|> Debug.log "Age"
44
, Cmd.none
45
)
46
47
48
subscriptions age =
49
Browser.Events.onAnimationFrameDelta identity
50
51
52
rules =
53
Dict.empty
54
|> Dict.insert "brown"
55
[ { color = "brown", rotation = 0 }
56
, { color = "green", rotation = 20 }
57
, { color = "green", rotation = -30 }
58
]
59
|> Dict.insert "green"
60
[ { color = "red", rotation = -45 }
61
, { color = "red", rotation = -5 }
62
, { color = "red", rotation = 50 }
63
]
64
65
66
segment age { color, rotation } =
67
if age <= 0 then
68
Svg.g [] []
69
70
else
71
Svg.g []
72
[ rules
73
|> Dict.get color
74
|> Maybe.withDefault []
75
|> List.map (segment (age - 1))
76
|> Svg.g
77
[ Svg.Attributes.transform
78
(String.concat
79
[ "rotate("
80
, String.fromFloat rotation
81
, ") translate("
82
, String.fromFloat (age * 10)
83
, ")"
84
]
85
)
86
]
87
, dot age color rotation
88
, line age color rotation
89
]
90
91
92
dot age color rotation =
93
Svg.circle
94
[ Svg.Attributes.r (String.fromFloat age)
95
, Svg.Attributes.cx "0"
96
, Svg.Attributes.cy "0"
97
, Svg.Attributes.fill color
98
, Svg.Attributes.transform
99
(String.concat
100
[ "rotate("
101
, String.fromFloat rotation
102
, ") translate("
103
, String.fromFloat (age * 10)
104
, ")"
105
]
106
)
107
]
108
[]
109
110
111
line age color rotation =
112
Svg.line
113
[ Svg.Attributes.strokeWidth "1"
114
, Svg.Attributes.x1 "0"
115
, Svg.Attributes.y1 "0"
116
, Svg.Attributes.x1 (String.fromFloat (age * 10))
117
, Svg.Attributes.y1 "0"
118
, Svg.Attributes.stroke color
119
, Svg.Attributes.strokeWidth (String.fromFloat age)
120
, Svg.Attributes.transform
121
(String.concat
122
[ "rotate("
123
, String.fromFloat rotation
124
, ")"
125
]
126
)
127
]
128
[]
129
130
131
That’s it!
Your tree is growing over time!
We hope you had good time learning programming with us. If we still have some time left, we can play with the code.