├── .gitignore
├── images
└── retro-bg.jpg
├── src
├── Cell.elm
├── Icons.elm
├── RleParser.elm
├── Cell
│ └── Collection.elm
└── GameOfLife.elm
├── elm.json
├── README.md
├── tests
└── RleParserTest.elm
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff/*
2 | elm.js
3 | .tool-versions
4 |
--------------------------------------------------------------------------------
/images/retro-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tennety/elm-game-of-life/HEAD/images/retro-bg.jpg
--------------------------------------------------------------------------------
/src/Cell.elm:
--------------------------------------------------------------------------------
1 | module Cell exposing (Cell, fromTuple, toTuple, transform, x, y)
2 |
3 | import Set exposing (Set)
4 | import Tuple
5 |
6 |
7 | type Cell
8 | = Cell Int Int
9 |
10 |
11 | toTuple : Cell -> ( Int, Int )
12 | toTuple (Cell x_ y_) =
13 | ( x_, y_ )
14 |
15 |
16 | fromTuple : ( Int, Int ) -> Cell
17 | fromTuple ( x_, y_ ) =
18 | Cell x_ y_
19 |
20 |
21 | x : Cell -> Int
22 | x (Cell x_ _) =
23 | x_
24 |
25 |
26 | y : Cell -> Int
27 | y (Cell _ y_) =
28 | y_
29 |
30 |
31 | transform : (Int -> Int) -> Cell -> Cell
32 | transform f (Cell x_ y_) =
33 | Cell (f x_) (f y_)
34 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/browser": "1.0.2",
10 | "elm/core": "1.0.2",
11 | "elm/html": "1.0.0",
12 | "elm/parser": "1.1.0",
13 | "elm/svg": "1.0.1",
14 | "elm/time": "1.0.0",
15 | "mdgriffith/elm-ui": "1.1.5"
16 | },
17 | "indirect": {
18 | "elm/json": "1.1.2",
19 | "elm/random": "1.0.0",
20 | "elm/url": "1.0.0",
21 | "elm/virtual-dom": "1.0.2"
22 | }
23 | },
24 | "test-dependencies": {
25 | "direct": {
26 | "elm-explorations/test": "1.2.2"
27 | },
28 | "indirect": {}
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elm Game of Life
2 |
3 | This project is a very basic implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life) in [Elm](https://elm-lang.org). I have been using it as a hobby project to teach myself Elm.
4 |
5 | The game features a square grid of 100 × 100 cells, with the edges wrapping around, so that cells on any edge treat cells on the opposite edge as neighbors. This is different from an "infinite" grid, so some patterns may not behave as expected. I used this technique for simplicity in calculating each cell's neighbors from one generation to the next.
6 |
7 | With the primary purpose of this project being learning the language, it is not yet optimized for performance or even accuracy. It is not meant as general purpose Game of Life simulation software. There is, however a [tested](tests/RleParserTest.elm) [RLE](https://www.conwaylife.com/wiki/Run_Length_Encoded) [parser](src/RleParser.elm) built into it, which can be used to seed the grid with patterns. There are also a few built-in patterns (which are also written in RLE and parsed).
8 |
9 | ## Running locally
10 | Because of the way I'm using flags to display the background, `elm reactor` doesn't quite work yet, and `elm make` has to be run manually:
11 | ```shell
12 | elm make src/GameOfLife.elm --output=elm.js --debug
13 | ```
14 | It's something I'm working on figuring out!
15 |
--------------------------------------------------------------------------------
/src/Icons.elm:
--------------------------------------------------------------------------------
1 | {- Icon code generated at https://1602.github.io/elm-feather-icons/ -}
2 |
3 |
4 | module Icons exposing
5 | ( externalLink
6 | , pause
7 | , play
8 | , skipBack
9 | )
10 |
11 | import Html exposing (Html)
12 | import Svg exposing (Svg, svg)
13 | import Svg.Attributes exposing (..)
14 |
15 |
16 | svgFeatherIcon : String -> List (Svg msg) -> Html msg
17 | svgFeatherIcon className =
18 | svg
19 | [ class <| "feather feather-" ++ className
20 | , fill "none"
21 | , height "24"
22 | , stroke "currentColor"
23 | , strokeLinecap "round"
24 | , strokeLinejoin "round"
25 | , strokeWidth "2"
26 | , viewBox "0 0 24 24"
27 | , width "24"
28 | ]
29 |
30 |
31 | pause : Html msg
32 | pause =
33 | svgFeatherIcon "pause"
34 | [ Svg.rect [ Svg.Attributes.x "6", y "4", width "4", height "16" ] []
35 | , Svg.rect [ Svg.Attributes.x "14", y "4", width "4", height "16" ] []
36 | ]
37 |
38 |
39 | play : Html msg
40 | play =
41 | svgFeatherIcon "play"
42 | [ Svg.polygon [ points "5 3 19 12 5 21 5 3" ] []
43 | ]
44 |
45 |
46 | skipBack : Html msg
47 | skipBack =
48 | svgFeatherIcon "skip-back"
49 | [ Svg.polygon [ points "19 20 9 12 19 4 19 20" ] []
50 | , Svg.line [ x1 "5", y1 "19", x2 "5", y2 "5" ] []
51 | ]
52 |
53 |
54 | externalLink : Html msg
55 | externalLink =
56 | svgFeatherIcon "external-link"
57 | [ Svg.path [ d "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" ] []
58 | , Svg.polyline [ points "15 3 21 3 21 9" ] []
59 | , Svg.line [ x1 "10", y1 "14", x2 "21", y2 "3" ] []
60 | ]
61 |
--------------------------------------------------------------------------------
/tests/RleParserTest.elm:
--------------------------------------------------------------------------------
1 | module RleParserTest exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Fuzz exposing (Fuzzer, int, list, string)
5 | import RleParser
6 | import Test exposing (..)
7 |
8 |
9 | suite : Test
10 | suite =
11 | describe "RLE Parser"
12 | [ test "parses one dead cell" <|
13 | \_ ->
14 | Expect.equal (Ok []) (RleParser.parse "b!")
15 | , test "parses multiple dead cells" <|
16 | \_ ->
17 | Expect.equal (Ok []) (RleParser.parse "3b!")
18 | , test "parses one live cell" <|
19 | \_ ->
20 | Expect.equal (Ok [ ( 0, 0 ) ]) (RleParser.parse "o!")
21 | , test "parses multiple live cells" <|
22 | \_ ->
23 | Expect.equal (Ok [ ( 0, 0 ), ( 1, 0 ), ( 2, 0 ) ]) (RleParser.parse "3o!")
24 | , test "parses combination of dead and live cells" <|
25 | \_ ->
26 | Expect.equal (Ok [ ( 0, 0 ), ( 1, 0 ), ( 2, 0 ) ]) (RleParser.parse "3o2b!")
27 | , test "parses multiple lines" <|
28 | \_ ->
29 | Expect.equal
30 | (Ok
31 | [ ( 0, 0 )
32 | , ( 1, 0 )
33 | , ( 2, 0 )
34 | , ( 0, 1 )
35 | , ( 2, 1 )
36 | ]
37 | )
38 | (RleParser.parse "3o$obo!")
39 | , test "parses blank lines" <|
40 | \_ ->
41 | Expect.equal (Ok [ ( 0, 0 ), ( 0, 2 ) ]) (RleParser.parse "o2$o!")
42 | , test "does not fail without trailing bang" <|
43 | \_ ->
44 | Expect.equal (Ok [ ( 0, 0 ), ( 1, 0 ), ( 0, 1 ) ]) (RleParser.parse "2o$ob")
45 | , test "errors on an invalid rle string" <|
46 | \_ ->
47 | Expect.err (RleParser.parse "$#df%%fsd$!$$")
48 | ]
49 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Elm Game of Life
8 |
40 |
41 |
42 |
43 |
44 |
48 |
52 |
53 |
54 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/RleParser.elm:
--------------------------------------------------------------------------------
1 | module RleParser exposing (parse)
2 |
3 | import Parser exposing (..)
4 |
5 |
6 | type alias GridState =
7 | { x : Int
8 | , y : Int
9 | , cellList : List ( Int, Int )
10 | }
11 |
12 |
13 | type CellState
14 | = Alive
15 | | Dead
16 | | EmptyLine
17 |
18 |
19 | init =
20 | { x = 0
21 | , y = 0
22 | , cellList = []
23 | }
24 |
25 |
26 | parse : String -> Result (List DeadEnd) (List ( Int, Int ))
27 | parse =
28 | run cells
29 |
30 |
31 | cells : Parser (List ( Int, Int ))
32 | cells =
33 | loop init cellHelp
34 |
35 |
36 | cellHelp : GridState -> Parser (Step GridState (List ( Int, Int )))
37 | cellHelp gridState =
38 | oneOf
39 | [ succeed (addCells gridState)
40 | |= int
41 | |= cellToken
42 | , succeed (addCells gridState 1)
43 | |= cellToken
44 | , succeed (nextLine gridState 1)
45 | |. token "$"
46 | , succeed (Done gridState.cellList)
47 | |. token "!"
48 | , succeed (Done gridState.cellList)
49 | |. end
50 | , problem "Invalid RLE string. I support only two states, so use `b` and `o` for cells"
51 | ]
52 |
53 |
54 | cellToken : Parser CellState
55 | cellToken =
56 | oneOf
57 | [ map (\_ -> Dead) (token "b")
58 | , map (\_ -> Alive) (token "o")
59 | , map (\_ -> EmptyLine) (token "$")
60 | ]
61 |
62 |
63 | addCells : GridState -> Int -> CellState -> Step GridState (List ( Int, Int ))
64 | addCells gridState count aliveOrDead =
65 | case aliveOrDead of
66 | Alive ->
67 | let
68 | newX =
69 | gridState.x + count
70 |
71 | xRange =
72 | List.range gridState.x (newX - 1)
73 |
74 | cellsToAdd =
75 | List.map (\i -> ( i, gridState.y )) xRange
76 |
77 | updatedState =
78 | { gridState | x = newX, cellList = gridState.cellList ++ cellsToAdd }
79 | in
80 | Loop updatedState
81 |
82 | Dead ->
83 | let
84 | newX =
85 | gridState.x + count
86 |
87 | updatedState =
88 | { gridState | x = newX }
89 | in
90 | Loop updatedState
91 |
92 | EmptyLine ->
93 | nextLine gridState count
94 |
95 |
96 | nextLine : GridState -> Int -> Step GridState (List ( Int, Int ))
97 | nextLine gridState count =
98 | let
99 | updatedState =
100 | { gridState | x = 0, y = gridState.y + count }
101 | in
102 | Loop updatedState
103 |
--------------------------------------------------------------------------------
/src/Cell/Collection.elm:
--------------------------------------------------------------------------------
1 | module Cell.Collection exposing (Collection, acorn, fromList, fromRle, frothingPuffer, gosperGliderGun, procreate, rpentomino, signal, toList)
2 |
3 | import Cell exposing (Cell, fromTuple, toTuple)
4 | import RleParser as Rle
5 | import Set exposing (Set)
6 |
7 |
8 | type Collection
9 | = Collection (Set ( Int, Int ))
10 |
11 |
12 | fromRle : String -> Collection
13 | fromRle rle =
14 | rle
15 | |> Rle.parse
16 | |> Result.withDefault []
17 | |> List.map Cell.fromTuple
18 | |> fromList
19 |
20 |
21 | fromList : List Cell -> Collection
22 | fromList =
23 | Collection << Set.fromList << List.map toTuple
24 |
25 |
26 | toList : Collection -> List Cell
27 | toList (Collection cells) =
28 | cells
29 | |> Set.toList
30 | |> List.map fromTuple
31 |
32 |
33 | procreate : Int -> Collection -> Collection
34 | procreate limit (Collection cells) =
35 | let
36 | familySize cell =
37 | Set.size <| Set.intersect (neighbors limit cell) cells
38 |
39 | survivalRule cell =
40 | List.member (familySize cell) [ 2, 3 ]
41 |
42 | birthRule cell =
43 | 3 == familySize cell
44 |
45 | willSurvive =
46 | Set.filter survivalRule cells
47 |
48 | willBeBorn =
49 | Set.filter birthRule (neighborhood limit cells)
50 | in
51 | Collection (Set.union willSurvive willBeBorn)
52 |
53 |
54 | rpentomino =
55 | fromRle "bob$2ob$b2o!"
56 |
57 |
58 | acorn =
59 | fromRle "2o2b3o$3bo3b$bo5b!"
60 |
61 |
62 | signal =
63 | fromRle "3o!"
64 |
65 |
66 | gosperGliderGun =
67 | fromRle "24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4bobo$10bo5bo7bo$11bo3bo$12b2o!"
68 |
69 |
70 | frothingPuffer =
71 | fromRle "7bo17bo$6b3o15b3o$5b2o4b3o5b3o4b2o$3b2obo2b3o2bo3bo2b3o2bob2o$4bobo2bobo3bobo3bobo2bobo$b2obobobobo4bobo4bobobobob2o$b2o3bobo4bo5bo4bobo3b2o$b3obo3bo4bobobo4bo3bob3o$2o9b2obobobob2o9b2o$12bo7bo$9b2obo7bob2o$10bo11bo$7b2obo11bob2o$7b2o15b2o$7bobobob3ob3obobobo$6b2o3bo3bobo3bo3b2o$6bo2bo3bobobobo3bo2bo$9b2o4bobo4b2o$5b2o4bo3bobo3bo4b2o$9bob2obo3bob2obo$10bobobobobobobo$12bo2bobo2bo$11bobo5bobo!"
72 |
73 |
74 |
75 | --- Internal API ---
76 |
77 |
78 | wrap : Int -> Int -> Int
79 | wrap limit value =
80 | if value < negate limit then
81 | limit
82 |
83 | else if value > limit then
84 | negate limit
85 |
86 | else
87 | value
88 |
89 |
90 | neighbors : Int -> ( Int, Int ) -> Set ( Int, Int )
91 | neighbors limit ( x_, y_ ) =
92 | let
93 | wrapAtLimit =
94 | wrap limit
95 | in
96 | Set.fromList
97 | [ ( wrapAtLimit (x_ - 1), wrapAtLimit (y_ + 1) )
98 | , ( x_, wrapAtLimit (y_ + 1) )
99 | , ( wrapAtLimit (x_ + 1), wrapAtLimit (y_ + 1) )
100 | , ( wrapAtLimit (x_ - 1), y_ )
101 | , ( wrapAtLimit (x_ + 1), y_ )
102 | , ( wrapAtLimit (x_ - 1), wrapAtLimit (y_ - 1) )
103 | , ( x_, wrapAtLimit (y_ - 1) )
104 | , ( wrapAtLimit (x_ + 1), wrapAtLimit (y_ - 1) )
105 | ]
106 |
107 |
108 | neighborhood limit cells =
109 | let
110 | filledHood =
111 | Set.foldl (Set.union << neighbors limit) Set.empty cells
112 | in
113 | Set.diff filledHood cells
114 |
115 |
116 |
117 | --- End Internal API ---
118 |
--------------------------------------------------------------------------------
/src/GameOfLife.elm:
--------------------------------------------------------------------------------
1 | module GameOfLife exposing (Flags, Model, Msg(..), RootUrl, State(..), appButton, cellForm, cellForms, init, initialModel, main, playButton, subscriptions, update, view, withBackground)
2 |
3 | import Browser
4 | import Browser.Events exposing (onAnimationFrameDelta)
5 | import Cell exposing (Cell, transform, x, y)
6 | import Cell.Collection as Cells exposing (acorn, fromRle, frothingPuffer, gosperGliderGun, procreate, rpentomino, toList)
7 | import Element as El exposing (centerX, column, el, fill, height, html, layout, padding, paddingXY, px, rgba255, row, spacing, text, width)
8 | import Element.Background as Bg exposing (image)
9 | import Element.Border as Border
10 | import Element.Font as Font
11 | import Element.Input exposing (button)
12 | import Element.Region exposing (heading)
13 | import Html exposing (Html)
14 | import Icons exposing (externalLink, pause, play, skipBack)
15 | import Set exposing (Set)
16 | import Svg exposing (..)
17 | import Svg.Attributes as SA exposing (..)
18 |
19 |
20 |
21 | -- Model --
22 |
23 |
24 | type State
25 | = Running
26 | | Paused
27 |
28 |
29 | type alias RootUrl =
30 | String
31 |
32 |
33 | type alias Model =
34 | { assetRoot : RootUrl
35 | , liveCells : Cells.Collection
36 | , state : State
37 | , fps : Int
38 | , userPattern : String
39 | }
40 |
41 |
42 | type alias Flags =
43 | { assetRoot : String
44 | }
45 |
46 |
47 | initialModel : Model
48 | initialModel =
49 | Model "/" Cells.frothingPuffer Paused 0 ""
50 |
51 |
52 | init : Flags -> ( Model, Cmd Msg )
53 | init flags =
54 | ( { initialModel | assetRoot = flags.assetRoot }, Cmd.none )
55 |
56 |
57 |
58 | -- Update --
59 |
60 |
61 | type Msg
62 | = Play
63 | | Pause
64 | | Reset
65 | | Tick Float
66 | | UserLoadedPattern Cells.Collection
67 | | UserEnteredPattern String
68 | | UserSubmittedPattern
69 |
70 |
71 | update : Msg -> Model -> ( Model, Cmd Msg )
72 | update msg model =
73 | case msg of
74 | Reset ->
75 | ( { initialModel | assetRoot = model.assetRoot }, Cmd.none )
76 |
77 | Play ->
78 | ( { model | state = Running }, Cmd.none )
79 |
80 | Pause ->
81 | ( { model | state = Paused }, Cmd.none )
82 |
83 | Tick interval ->
84 | ( { model | liveCells = procreate 100 model.liveCells, fps = round (1000 / interval) }, Cmd.none )
85 |
86 | UserLoadedPattern cells ->
87 | ( { model | liveCells = cells }, Cmd.none )
88 |
89 | UserEnteredPattern rle ->
90 | ( { model | userPattern = rle }, Cmd.none )
91 |
92 | UserSubmittedPattern ->
93 | let
94 | trimmed =
95 | String.lines >> String.join ""
96 | in
97 | ( { model | liveCells = fromRle (trimmed model.userPattern) }, Cmd.none )
98 |
99 |
100 |
101 | -- View Helpers --
102 |
103 |
104 | cellForm : Float -> Int -> Int -> Cell -> Svg Msg
105 | cellForm radius scale translate cell =
106 | let
107 | scaled =
108 | Cell.transform (\coord -> coord * scale + translate)
109 | in
110 | circle
111 | [ cx (cell |> scaled |> Cell.x |> String.fromInt)
112 | , cy (cell |> scaled |> Cell.y |> String.fromInt)
113 | , r (String.fromFloat radius)
114 | , SA.fill "#333333"
115 | ]
116 | []
117 |
118 |
119 | cellForms : Cells.Collection -> List (Svg Msg)
120 | cellForms liveCells =
121 | List.map (cellForm 1.5 3 300) (Cells.toList liveCells)
122 |
123 |
124 | withBackground : Int -> Int -> List (Svg Msg) -> List (Svg Msg)
125 | withBackground width height cells =
126 | List.append
127 | [ rect
128 | [ SA.x "0"
129 | , SA.y "0"
130 | , SA.width <| String.fromInt width
131 | , SA.height <| String.fromInt height
132 | , SA.fill "#ffffff"
133 | , rx "5"
134 | , ry "5"
135 | ]
136 | []
137 | ]
138 | cells
139 |
140 |
141 | appButton : Msg -> Svg Msg -> El.Element Msg
142 | appButton msg icon =
143 | button
144 | [ paddingXY 10 5
145 | , El.width El.fill
146 | , El.height (px 34)
147 | , Border.rounded 4
148 | , Border.width 1
149 | , Border.color (rgba255 20 20 20 1)
150 | , Bg.color (rgba255 220 255 255 0.7)
151 | ]
152 | { label = html icon, onPress = Just msg }
153 |
154 |
155 | playButton : State -> El.Element Msg
156 | playButton state =
157 | case state of
158 | Running ->
159 | appButton Pause Icons.pause
160 |
161 | Paused ->
162 | appButton Play Icons.play
163 |
164 |
165 | formButton : Msg -> Bool -> El.Element Msg -> El.Element Msg
166 | formButton msg selected label =
167 | let
168 | borderOpacity =
169 | if selected then
170 | 1
171 |
172 | else
173 | 0
174 | in
175 | button
176 | [ paddingXY 10 5
177 | , El.width El.fill
178 | , El.height (px 34)
179 | , Border.rounded 4
180 | , Border.width 2
181 | , Border.color (rgba255 20 20 20 borderOpacity)
182 | , Bg.color (rgba255 220 255 255 0.7)
183 | ]
184 | { label = label, onPress = Just msg }
185 |
186 |
187 |
188 | -- View --
189 |
190 |
191 | view : Model -> Html Msg
192 | view model =
193 | layout [ Bg.image <| model.assetRoot ++ "images/retro-bg.jpg" ] <|
194 | column
195 | [ El.height El.fill, El.width El.fill, centerX, Font.family [ Font.typeface "Courier", Font.monospace ] ]
196 | [ column
197 | [ centerX, El.spacing 10, padding 10, El.width El.fill, Bg.color (rgba255 20 20 20 0.7), Font.color (rgba255 200 200 200 1) ]
198 | [ el [ centerX, heading 1, Font.size 30 ] (El.text "Conway's Game of Life")
199 | , row
200 | [ centerX ]
201 | [ El.text "Dedicated to "
202 | , El.newTabLink
203 | []
204 | { url = "https://en.wikipedia.org/wiki/John_Horton_Conway#Death"
205 | , label = El.text "John Conway"
206 | }
207 | , el [ El.height (px 20), Font.color (rgba255 150 150 150 0.8) ] (html externalLink)
208 | ]
209 | ]
210 | , row
211 | [ El.width El.fill ]
212 | [ world model
213 | , form model
214 | ]
215 | ]
216 |
217 |
218 | world : Model -> El.Element Msg
219 | world model =
220 | column
221 | [ padding 50, El.width (El.fillPortion 3) ]
222 | [ row
223 | [ El.alignRight, El.height (px 600) ]
224 | [ model.liveCells
225 | |> cellForms
226 | |> withBackground 600 600
227 | |> svg [ viewBox "0 0 600 600", SA.width "600px" ]
228 | |> html
229 | ]
230 | , row
231 | [ El.alignRight, padding 5, El.spacing 5 ]
232 | [ el [ centerX ] (El.text <| "fps:" ++ (model.fps |> String.fromInt))
233 | , playButton model.state
234 | , appButton Reset Icons.skipBack
235 | ]
236 | ]
237 |
238 |
239 | form : Model -> El.Element Msg
240 | form model =
241 | column
242 | [ centerX, padding 50, El.height El.fill, El.width (El.fillPortion 2) ]
243 | [ column
244 | [ El.width (El.fill |> El.maximum 400), Bg.color (rgba255 255 255 255 0.7), padding 20, Border.rounded 10 ]
245 | [ el [ centerX ] (El.text "Select a form")
246 | , El.wrappedRow
247 | [ centerX, padding 5, El.spacing 5 ]
248 | [ formButton (UserLoadedPattern rpentomino) (model.liveCells == rpentomino) (El.text "R-Pentomino")
249 | , formButton (UserLoadedPattern acorn) (model.liveCells == acorn) (El.text "Acorn")
250 | , formButton (UserLoadedPattern gosperGliderGun) (model.liveCells == gosperGliderGun) (El.text "Gosper Glider Gun")
251 | , formButton (UserLoadedPattern frothingPuffer) (model.liveCells == frothingPuffer) (El.text "Frothing Puffer")
252 | ]
253 | , el [ centerX, padding 20 ] (El.text "or")
254 | , Element.Input.multiline
255 | [ centerX, padding 10, El.height (El.fill |> El.maximum 200) ]
256 | { onChange = UserEnteredPattern
257 | , text = model.userPattern
258 | , placeholder = Just <| Element.Input.placeholder [] (El.text "bob$2ob$b2o!")
259 | , label = Element.Input.labelAbove [ centerX ] (El.text "Type or paste RLE below")
260 | , spellcheck = False
261 | }
262 | , row
263 | [ centerX, padding 5, El.spacing 5 ]
264 | [ formButton UserSubmittedPattern True (El.text "Submit") ]
265 | , El.paragraph
266 | [ El.paddingEach { top = 30, right = 0, bottom = 10, left = 0 } ]
267 | [ El.text "You can find a list of RLE pattern files "
268 | , El.newTabLink
269 | []
270 | { url = "https://copy.sh/life/examples/"
271 | , label = El.text "here."
272 | }
273 | , el [ El.height (px 20), Font.color (rgba255 50 50 50 0.8) ] (html externalLink)
274 | ]
275 | ]
276 | ]
277 |
278 |
279 |
280 | -- SUBSCRIPTIONS
281 |
282 |
283 | subscriptions : Model -> Sub Msg
284 | subscriptions model =
285 | case model.state of
286 | Paused ->
287 | Sub.none
288 |
289 | Running ->
290 | onAnimationFrameDelta Tick
291 |
292 |
293 |
294 | -- Main --
295 |
296 |
297 | main =
298 | Browser.element
299 | { init = init
300 | , view = view
301 | , update = update
302 | , subscriptions = subscriptions
303 | }
304 |
--------------------------------------------------------------------------------