├── .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 | --------------------------------------------------------------------------------