├── API_DESIGN_DECISIONS.md ├── .gitignore ├── examples ├── Drawing.elm ├── Animation.elm ├── Simulation.elm └── Interaction.elm ├── elm-package.json ├── README.md └── src └── Collage └── Program.elm /API_DESIGN_DECISIONS.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | documentation.json 3 | -------------------------------------------------------------------------------- /examples/Drawing.elm: -------------------------------------------------------------------------------- 1 | module Drawing exposing (..) 2 | 3 | import Color exposing (..) 4 | import Collage exposing (..) 5 | import Collage.Program exposing (draw, Drawing) 6 | 7 | 8 | view : Form 9 | view = 10 | filled red (circle 50) 11 | 12 | 13 | main : Drawing 14 | main = 15 | draw view 16 | -------------------------------------------------------------------------------- /examples/Animation.elm: -------------------------------------------------------------------------------- 1 | module Animation exposing (..) 2 | 3 | import Color exposing (..) 4 | import Collage exposing (..) 5 | import Collage.Program exposing (animate, Animation) 6 | 7 | 8 | view : Float -> Form 9 | view time = 10 | let 11 | seconds = 12 | time / 1000 13 | 14 | radius = 15 | 50 16 | 17 | y = 18 | (sin seconds) * 50 19 | in 20 | move ( 0, y ) (filled red (circle radius)) 21 | 22 | 23 | main : Animation 24 | main = 25 | animate view 26 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "summary": "Gradual introduction to interactive graphics programs.", 4 | "repository": "https://github.com/john-kelly/elm-interactive-graphics.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Collage.Program" 11 | ], 12 | "dependencies": { 13 | "elm-lang/animation-frame": "1.0.1 <= v < 2.0.0", 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 15 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 16 | "elm-lang/keyboard": "1.0.1 <= v < 2.0.0", 17 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0", 18 | "elm-lang/window": "1.0.1 <= v < 2.0.0", 19 | "evancz/elm-graphics": "1.0.1 <= v < 2.0.0" 20 | }, 21 | "elm-version": "0.18.0 <= v < 0.19.0" 22 | } 23 | -------------------------------------------------------------------------------- /examples/Simulation.elm: -------------------------------------------------------------------------------- 1 | module Simulation exposing (..) 2 | 3 | import Color exposing (..) 4 | import Collage exposing (..) 5 | import Collage.Program exposing (simulate, Simulation) 6 | 7 | 8 | type alias Model = 9 | ( Float, Float, Float, Float, Float ) 10 | 11 | 12 | init : Model 13 | init = 14 | ( 0, 0, 2, 1, 30 ) 15 | 16 | 17 | view : Model -> Form 18 | view ( x, y, dx, dy, radius ) = 19 | move ( x, y ) (filled red (circle radius)) 20 | 21 | 22 | update : Float -> Model -> Model 23 | update time ( x, y, dx, dy, radius ) = 24 | if x + radius > 200 || x - radius < -200 then 25 | ( x - dx, y + dy, -dx, dy, radius ) 26 | else if y + radius > 200 || y - radius < -200 then 27 | ( x + dx, y - dy, dx, -dy, radius ) 28 | else 29 | ( x + dx, y + dy, dx, dy, radius ) 30 | 31 | 32 | main : Simulation Model 33 | main = 34 | simulate 35 | { init = init 36 | , view = view 37 | , update = update 38 | } 39 | -------------------------------------------------------------------------------- /examples/Interaction.elm: -------------------------------------------------------------------------------- 1 | module Interaction exposing (..) 2 | 3 | import Color exposing (..) 4 | import Collage exposing (..) 5 | import Collage.Program exposing (interact, Interaction, Msg(..)) 6 | 7 | 8 | type alias Model = 9 | ( Float, Float, Float, Float, Float ) 10 | 11 | 12 | init : Model 13 | init = 14 | ( 0, 0, 2, 1, 30 ) 15 | 16 | 17 | view : Model -> Form 18 | view ( x, y, dx, dy, radius ) = 19 | move ( x, y ) (filled red (circle radius)) 20 | 21 | 22 | update : Msg -> Model -> Model 23 | update msg (( x, y, dx, dy, radius ) as model) = 24 | case msg of 25 | TimeTick _ -> 26 | if x + radius > 200 || x - radius < -200 then 27 | ( x - dx, y + dy, -dx, dy, radius ) 28 | else if y + radius > 200 || y - radius < -200 then 29 | ( x + dx, y - dy, dx, -dy, radius ) 30 | else 31 | ( x + dx, y + dy, dx, dy, radius ) 32 | 33 | MouseClick _ -> 34 | ( x, y, -dx, dy, radius ) 35 | 36 | _ -> 37 | model 38 | 39 | 40 | main : Interaction Model 41 | main = 42 | interact 43 | { init = init 44 | , view = view 45 | , update = update 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-interactive-graphics 2 | 3 | Gradual introduction to interactive graphics programs. 4 | 5 | #### The Steps: 6 | 1. Drawing: draw elements 7 | 2. Animation: draw with time 8 | 3. Simulation: draw with time and model 9 | 4. Interaction: draw with time and msgs 10 | 11 | ## Sample Simulation Program 12 | ```elm 13 | import Color exposing (..) 14 | import Collage exposing (..) 15 | import Collage.Program exposing (simulate, Simulation) 16 | 17 | 18 | type alias Model = 19 | ( Float, Float, Float, Float, Float ) 20 | 21 | 22 | init : Model 23 | init = 24 | ( 0, 0, 2, 1, 30 ) 25 | 26 | 27 | view : Model -> Form 28 | view ( x, y, dx, dy, radius ) = 29 | move ( x, y ) (filled red (circle radius)) 30 | 31 | 32 | update : Float -> Model -> Model 33 | update time ( x, y, dx, dy, radius ) = 34 | if x + radius > 200 || x - radius < -200 then 35 | ( x - dx, y + dy, -dx, dy, radius ) 36 | else if y + radius > 200 || y - radius < -200 then 37 | ( x + dx, y - dy, dx, -dy, radius ) 38 | else 39 | ( x + dx, y + dy, dx, dy, radius ) 40 | 41 | 42 | main : Simulation Model 43 | main = 44 | simulate 45 | { init = init 46 | , view = view 47 | , update = update 48 | } 49 | ``` 50 | 51 | # Influences: 52 | - https://github.com/jcollard/elm-playground 53 | - https://github.com/evancz/elm-playground 54 | - https://github.com/MacCASOutreach/graphicsvg/tree/1.1.1 55 | - https://github.com/jvoigtlaender/Elm-Kurs/blob/master/src/lib/Lib.elm 56 | - https://github.com/google/codeworld 57 | -------------------------------------------------------------------------------- /src/Collage/Program.elm: -------------------------------------------------------------------------------- 1 | module Collage.Program 2 | exposing 3 | ( Drawing 4 | , draw 5 | , Animation 6 | , animate 7 | , Simulation 8 | , simulate 9 | , Interaction 10 | , interact 11 | , Msg(..) 12 | , Mouse 13 | , Key(..) 14 | , Window 15 | ) 16 | 17 | {-| Gradual introduction to interactive graphics programs. 18 | 19 | 20 | # Draw 21 | 22 | @docs Drawing, draw 23 | 24 | 25 | # Draw with Time 26 | 27 | @docs Animation, animate 28 | 29 | 30 | # Draw with Time and Model 31 | 32 | @docs Simulation, simulate 33 | 34 | 35 | # Draw with Model and Messages 36 | 37 | @docs Interaction, interact, Msg, Mouse, Key, Window 38 | 39 | -} 40 | 41 | import AnimationFrame 42 | import Element 43 | import Collage 44 | import Html 45 | import Html.Lazy exposing (lazy3) 46 | import Keyboard exposing (KeyCode) 47 | import Mouse exposing (Position) 48 | import Task 49 | import Time exposing (Time) 50 | import Window exposing (Size) 51 | 52 | 53 | {-| -} 54 | type alias Mouse = 55 | { x : Float, y : Float } 56 | 57 | 58 | {-| -} 59 | type Key 60 | = Tab 61 | | Enter 62 | | Shift 63 | | Space 64 | | Left 65 | | Up 66 | | Right 67 | | Down 68 | | Zero 69 | | One 70 | | Two 71 | | Three 72 | | Four 73 | | Five 74 | | Six 75 | | Seven 76 | | Eight 77 | | Nine 78 | | A 79 | | B 80 | | C 81 | | D 82 | | E 83 | | F 84 | | G 85 | | H 86 | | I 87 | | J 88 | | K 89 | | L 90 | | M 91 | | N 92 | | O 93 | | P 94 | | Q 95 | | R 96 | | S 97 | | T 98 | | U 99 | | V 100 | | W 101 | | X 102 | | Y 103 | | Z 104 | | Other KeyCode 105 | 106 | 107 | {-| -} 108 | type alias Window = 109 | { top : Float 110 | , bottom : Float 111 | , left : Float 112 | , right : Float 113 | } 114 | 115 | 116 | {-| -} 117 | type Msg 118 | = TimeTick Time 119 | | MouseClick Mouse 120 | | MouseDown Mouse 121 | | MouseUp Mouse 122 | | MouseMove Mouse 123 | | KeyDown Key 124 | | KeyUp Key 125 | | WindowResize Window 126 | 127 | 128 | type alias Model model = 129 | { window : Window 130 | , time : Time 131 | , model : model 132 | } 133 | 134 | 135 | {-| -} 136 | type alias Drawing = 137 | Program Never (Model ()) Msg 138 | 139 | 140 | {-| -} 141 | type alias Animation = 142 | Program Never (Model ()) Msg 143 | 144 | 145 | {-| -} 146 | type alias Simulation model = 147 | Program Never (Model model) Msg 148 | 149 | 150 | {-| -} 151 | type alias Interaction model = 152 | Program Never (Model model) Msg 153 | 154 | 155 | {-| -} 156 | draw : Collage.Form -> Drawing 157 | draw view = 158 | Html.program 159 | { init = wrapStudentInit () 160 | , view = \{ window } -> viewModelWindowToHtml (\_ -> view) () window 161 | , update = drawUpdate 162 | , subscriptions = \_ -> windowResizeSub 163 | } 164 | 165 | 166 | drawUpdate : Msg -> Model () -> ( Model (), Cmd Msg ) 167 | drawUpdate msg model = 168 | case msg of 169 | WindowResize newWindow -> 170 | { model | window = newWindow } ! [] 171 | 172 | _ -> 173 | model ! [] 174 | 175 | 176 | {-| -} 177 | animate : (Time -> Collage.Form) -> Animation 178 | animate view = 179 | Html.program 180 | { init = wrapStudentInit () 181 | , view = \{ time, window } -> viewModelWindowToHtml view time window 182 | , update = animateUpdate 183 | , subscriptions = animateSubs 184 | } 185 | 186 | 187 | animateUpdate : Msg -> Model () -> ( Model (), Cmd Msg ) 188 | animateUpdate msg model = 189 | case msg of 190 | TimeTick newTime -> 191 | { model | time = newTime } ! [] 192 | 193 | WindowResize newWindow -> 194 | { model | window = newWindow } ! [] 195 | 196 | _ -> 197 | model ! [] 198 | 199 | 200 | animateSubs : Model () -> Sub Msg 201 | animateSubs { time } = 202 | Sub.batch 203 | [ accumTimeSub time 204 | , windowResizeSub 205 | ] 206 | 207 | 208 | {-| -} 209 | simulate : 210 | { init : model 211 | , view : model -> Collage.Form 212 | , update : Time -> model -> model 213 | } 214 | -> Simulation model 215 | simulate { init, view, update } = 216 | Html.program 217 | { init = wrapStudentInit init 218 | , view = \{ model, window } -> lazy3 viewModelWindowToHtml view model window 219 | , update = simulateUpdate update 220 | , subscriptions = simulateSubs 221 | } 222 | 223 | 224 | simulateUpdate : (Time -> model -> model) -> Msg -> Model model -> ( Model model, Cmd Msg ) 225 | simulateUpdate update msg ({ model } as simulateModel) = 226 | case msg of 227 | TimeTick newTime -> 228 | let 229 | updatedModel = 230 | update newTime model 231 | 232 | newModel = 233 | -- necessary for lazy 234 | if updatedModel == model then 235 | model 236 | else 237 | updatedModel 238 | in 239 | { simulateModel | time = newTime, model = newModel } ! [] 240 | 241 | WindowResize newWindow -> 242 | { simulateModel | window = newWindow } ! [] 243 | 244 | _ -> 245 | simulateModel ! [] 246 | 247 | 248 | simulateSubs : Model model -> Sub Msg 249 | simulateSubs { time } = 250 | Sub.batch 251 | [ accumTimeSub time 252 | , windowResizeSub 253 | ] 254 | 255 | 256 | {-| -} 257 | interact : 258 | { init : model 259 | , view : model -> Collage.Form 260 | , update : Msg -> model -> model 261 | } 262 | -> Interaction model 263 | interact { init, view, update } = 264 | Html.program 265 | { init = wrapStudentInit init 266 | , view = \{ model, window } -> lazy3 viewModelWindowToHtml view model window 267 | , update = interactUpdate update 268 | , subscriptions = interactSubs 269 | } 270 | 271 | 272 | interactUpdate : (Msg -> model -> model) -> Msg -> Model model -> ( Model model, Cmd Msg ) 273 | interactUpdate update msg ({ model } as interactModel) = 274 | let 275 | updatedModel = 276 | update msg model 277 | 278 | newModel = 279 | -- necessary for lazy 280 | if updatedModel == model then 281 | model 282 | else 283 | updatedModel 284 | in 285 | case msg of 286 | TimeTick newTime -> 287 | { interactModel | time = newTime, model = newModel } ! [] 288 | 289 | WindowResize newWindow -> 290 | { interactModel | window = newWindow, model = newModel } ! [] 291 | 292 | _ -> 293 | { interactModel | model = newModel } ! [] 294 | 295 | 296 | interactSubs : Model model -> Sub Msg 297 | interactSubs { time, window } = 298 | Sub.batch 299 | [ accumTimeSub time 300 | , Mouse.clicks ((mouseToCartCoord window) >> MouseClick) 301 | , Mouse.downs ((mouseToCartCoord window) >> MouseDown) 302 | , Mouse.ups ((mouseToCartCoord window) >> MouseUp) 303 | , Mouse.moves ((mouseToCartCoord window) >> MouseMove) 304 | , Keyboard.downs (toKey >> KeyDown) 305 | , Keyboard.ups (toKey >> KeyUp) 306 | , windowResizeSub 307 | ] 308 | 309 | 310 | {-| Necessary for lazy! We need to define at the top level so that the 311 | reference of the lazy fn is the same across calls. 312 | 313 | From : 314 | So we just check to see if fn (viewModelWindowToHtml) and args (view, model, and window) are 315 | the same as last frame by comparing the old and new values by reference. This is 316 | super cheap, and if they are the same, the lazy function can often avoid a ton 317 | of work. This is a pretty simple trick that can speed things up significantly. 318 | 319 | -} 320 | viewModelWindowToHtml : (model -> Collage.Form) -> model -> Window -> Html.Html msg 321 | viewModelWindowToHtml view model window = 322 | let 323 | { width, height } = 324 | windowToSize window 325 | in 326 | Collage.collage width height [ view model ] 327 | |> Element.toHtml 328 | 329 | 330 | wrapStudentInit : model -> ( Model model, Cmd Msg ) 331 | wrapStudentInit model = 332 | let 333 | windowCmd = 334 | Task.perform (sizeToWindow >> WindowResize) Window.size 335 | in 336 | Model (Window 0 0 0 0) 0 model ! [ windowCmd ] 337 | 338 | 339 | windowResizeSub : Sub Msg 340 | windowResizeSub = 341 | Window.resizes (sizeToWindow >> WindowResize) 342 | 343 | 344 | accumTimeSub : Time -> Sub Msg 345 | accumTimeSub time = 346 | AnimationFrame.diffs ((\diff -> diff + time) >> TimeTick) 347 | 348 | 349 | mouseToCartCoord : Window -> Position -> Mouse 350 | mouseToCartCoord { right, top } { x, y } = 351 | { x = toFloat x - right, y = top - toFloat y } 352 | 353 | 354 | sizeToWindow : Size -> Window 355 | sizeToWindow { width, height } = 356 | let 357 | w = 358 | toFloat width / 2 359 | 360 | h = 361 | toFloat height / 2 362 | in 363 | { top = h 364 | , bottom = -h 365 | , left = -w 366 | , right = w 367 | } 368 | 369 | 370 | windowToSize : Window -> Size 371 | windowToSize { top, bottom, left, right } = 372 | let 373 | width = 374 | round (right - left) 375 | 376 | height = 377 | round (top - bottom) 378 | in 379 | { width = width, height = height } 380 | 381 | 382 | toKey : KeyCode -> Key 383 | toKey keyCode = 384 | case keyCode of 385 | 9 -> 386 | Tab 387 | 388 | 13 -> 389 | Enter 390 | 391 | 16 -> 392 | Shift 393 | 394 | 32 -> 395 | Space 396 | 397 | 37 -> 398 | Left 399 | 400 | 38 -> 401 | Up 402 | 403 | 39 -> 404 | Right 405 | 406 | 40 -> 407 | Down 408 | 409 | 48 -> 410 | Zero 411 | 412 | 49 -> 413 | One 414 | 415 | 50 -> 416 | Two 417 | 418 | 51 -> 419 | Three 420 | 421 | 52 -> 422 | Four 423 | 424 | 53 -> 425 | Five 426 | 427 | 54 -> 428 | Six 429 | 430 | 55 -> 431 | Seven 432 | 433 | 56 -> 434 | Eight 435 | 436 | 57 -> 437 | Nine 438 | 439 | 65 -> 440 | A 441 | 442 | 66 -> 443 | B 444 | 445 | 67 -> 446 | C 447 | 448 | 68 -> 449 | D 450 | 451 | 69 -> 452 | E 453 | 454 | 70 -> 455 | F 456 | 457 | 71 -> 458 | G 459 | 460 | 72 -> 461 | H 462 | 463 | 73 -> 464 | I 465 | 466 | 74 -> 467 | J 468 | 469 | 75 -> 470 | K 471 | 472 | 76 -> 473 | L 474 | 475 | 77 -> 476 | M 477 | 478 | 78 -> 479 | N 480 | 481 | 79 -> 482 | O 483 | 484 | 80 -> 485 | P 486 | 487 | 81 -> 488 | Q 489 | 490 | 82 -> 491 | R 492 | 493 | 83 -> 494 | S 495 | 496 | 84 -> 497 | T 498 | 499 | 85 -> 500 | U 501 | 502 | 86 -> 503 | V 504 | 505 | 87 -> 506 | W 507 | 508 | 88 -> 509 | X 510 | 511 | 89 -> 512 | Y 513 | 514 | 90 -> 515 | Z 516 | 517 | _ -> 518 | Other keyCode 519 | --------------------------------------------------------------------------------