Upgrade to Pro — share decks privately, control downloads, hide ads and more …

1st class web development with lineman

1st class web development with lineman

As was presented at Øredev 2013. This talk is more fun in video form: http://blog.testdouble.com/posts/2013-11-12-1st-class-web-development-with-lineman.html

Check out Lineman at http://linemanjs.com

Justin Searls

November 08, 2013
Tweet

More Decks by Justin Searls

Other Decks in Programming

Transcript

  1. 1st class web
    development
    with lineman

    View full-size slide

  2. My name is Justin Searls
    Please tweet me @searls &
    Say [email protected]

    View full-size slide

  3. @linemanjs
    linemanjs.com

    View full-size slide

  4. Java Developers

    View full-size slide

  5. Java Developers

    View full-size slide

  6. Ruby Developers

    View full-size slide

  7. Ruby Developers

    View full-size slide

  8. .NET Developers

    View full-size slide

  9. .NET Developers

    View full-size slide

  10. JavaScript Developers

    View full-size slide

  11. JavaScript Developers

    View full-size slide

  12. JavaScript Developers

    View full-size slide

  13. JavaScript Developers

    View full-size slide

  14. JavaScript Developers

    View full-size slide

  15. Inefficiency

    View full-size slide

  16. Browser capability

    View full-size slide

  17. Browser capability
    Opportunity cost

    View full-size slide

  18. Browser capability
    Opportunity cost

    View full-size slide

  19. Rails won the war.

    View full-size slide

  20. Rails won the war?
    which

    View full-size slide

  21. "Good frameworks
    are extractions,
    not inventions."
    - DHH

    View full-size slide

  22. Routing
    Models

    View full-size slide

  23. Routing
    Models
    Persistence

    View full-size slide

  24. Routing
    Models
    Persistence
    Sessions

    View full-size slide

  25. Routing
    Models
    Persistence
    Sessions
    Mailers

    View full-size slide

  26. Routing
    Models
    Persistence
    Sessions
    Mailers
    JS-free
    Dynamism

    View full-size slide

  27. HTML UI
    !
    Item 1

    View full-size slide

  28. HTML UI
    !
    Item 1
    !



    View full-size slide

  29. JavaScript UI

    View full-size slide

  30. JavaScript UI
    !
    new ItemView('#items').render()

    View full-size slide

  31. JavaScript UI
    !
    new ItemView('#items').render()
    !
    $('button').click(function(e){
    $('.alert').text('Thanks!').show()
    });

    View full-size slide

  32. HTML UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    └── views
    └── items
    └── index.html.erb

    View full-size slide

  33. HTML UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    ├── views
    │ └── items
    │ └── index.html.erb
    └── assets
    └── javascripts
    └── application.js

    View full-size slide

  34. HTML UI + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    ├── views
    │ └── items
    │ └── index.html.erb
    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs

    View full-size slide

  35. HTML UI + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    ├── views
    │ └── items
    │ └── index.html.erb
    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs

    View full-size slide

  36. HTML UI + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    ├── views
    │ └── items
    │ └── index.html.erb
    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs

    View full-size slide

  37. HTML UI + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb
    ├── views
    │ └── items
    │ └── index.html.erb
    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs

    View full-size slide

  38. JSON API + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb



    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs

    View full-size slide

  39. JSON API + JS UI
    !
    app
    ├── controllers
    │ └── items_controller.rb
    ├── models
    │ └── item.rb



    └── assets
    └── javascripts
    └── more_app
    ├── controllers
    │ └── items_controller.js
    ├── models
    │ └── item.js
    └── views
    └── items
    └── index.jst.ejs
    vestigial appendage

    View full-size slide

  40. what's wrong
    with vestigial
    appendages?

    View full-size slide

  41. late extraction
    costs more than
    early abstraction

    View full-size slide

  42. late extraction
    costs more than
    early abstraction

    View full-size slide

  43. When you see this:

    View full-size slide

  44. When you see this:
    !
    <br/>

    View full-size slide

  45. When you see this:
    !
    <br/>user = <%= user.to_json %><br/>

    View full-size slide

  46. When you see this:
    !
    <br/>user = <%= user.to_json %><br/>time = <%= Time.now.to_i %><br/>

    View full-size slide

  47. When you see this:
    !
    <br/>user = <%= user.to_json %><br/>time = <%= Time.now.to_i %><br/>items = <%= items.to_json %><br/>

    View full-size slide

  48. When you see this:
    !
    <br/>user = <%= user.to_json %><br/>time = <%= Time.now.to_i %><br/>items = <%= items.to_json %><br/>token = "<%= @token %>"<br/>

    View full-size slide

  49. When you see this:
    !
    <br/>user = <%= user.to_json %><br/>time = <%= Time.now.to_i %><br/>items = <%= items.to_json %><br/>token = "<%= @token %>"<br/>
    your API is broken

    View full-size slide

  50. We think we're

    View full-size slide

  51. But sometimes we're

    View full-size slide

  52. mature ≈ convenient

    View full-size slide

  53. Before Rails
    was easy

    View full-size slide

  54. Before Rails
    was easy
    JavaScript
    wasn't hard

    View full-size slide

  55. Just kidding.

    View full-size slide

  56. Do the Simplest Thing

    View full-size slide

  57. Do the Simplest Thing
    1. Show data in a table

    View full-size slide

  58. Do the Simplest Thing
    1. Show data in a table
    2. Now render it as a graph

    View full-size slide

  59. Do the Simplest Thing
    1. Show data in a table
    2. Now render it as a graph
    3. (ͮŇ㷩㷩Ň)ͮ

    View full-size slide

  60. Do the Simplest Thing
    1. Show data in a table
    2. Now render it as a graph
    3. (ͮŇ㷩㷩Ň)ͮ
    4. Now add filters, sliders, &
    zoom to the graph!

    View full-size slide

  61. Do the Simplest Thing
    1. Show data in a table
    2. Now render it as a graph
    3. (ͮŇ㷩㷩Ň)ͮ
    4. Now add filters, sliders, &
    zoom to the graph!
    5. (ಠ_ಠ)

    View full-size slide

  62. the simple cliff

    View full-size slide

  63. the simple cliff

    View full-size slide

  64. the simple cliff

    View full-size slide

  65. the simple cliff

    View full-size slide

  66. the simple cliff

    View full-size slide

  67. the simple cliff
    (›°□°ʣ›ớ

    View full-size slide

  68. Compete on easy.

    View full-size slide

  69. App Framework
    Convention & Config
    Build Automation

    View full-size slide

  70. Rails
    App Framework
    Convention & Config
    Build Automation

    View full-size slide

  71. Rails
    Rails
    App Framework
    Convention & Config
    Build Automation

    View full-size slide

  72. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation

    View full-size slide

  73. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation
    Backbone

    View full-size slide

  74. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation
    Backbone
    Ember

    View full-size slide

  75. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation
    Backbone
    Ember
    Angular

    View full-size slide

  76. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation
    Backbone
    Ember
    Angular
    ᵇ(´-ʆ)ᵃ


    View full-size slide

  77. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation Grunt (Node.js)
    Backbone
    Ember
    Angular
    ᵇ(´-ʆ)ᵃ


    View full-size slide

  78. Rails
    Rails
    Railsy Rake
    App Framework
    Convention & Config
    Build Automation Grunt (Node.js)
    Lineman
    Backbone
    Ember
    Angular
    ᵇ(´-ʆ)ᵃ


    View full-size slide

  79. !
    $ npm install -g lineman
    Install

    View full-size slide

  80. !
    $ npm install -g lineman
    Install
    !
    $ lineman new my-app
    Create

    View full-size slide

  81. ;?❤️! # @ ;

    View full-size slide

  82. ;?❤️! # @ ;
    ☕-script!

    View full-size slide

  83. }
    make
    Cmake
    Rake
    Ant / NAnt
    MSBuild
    Maven
    Grunt

    View full-size slide

  84. Easy peasy
    !
    $ cat tasks/hello.js
    !

    View full-size slide

  85. Easy peasy
    !
    $ cat tasks/hello.js
    !
    module.exports = function(grunt) {
    };

    View full-size slide

  86. Easy peasy
    !
    $ cat tasks/hello.js
    !
    module.exports = function(grunt) {
    grunt.registerTask("hello",function(){
    });
    };

    View full-size slide

  87. Easy peasy
    !
    $ cat tasks/hello.js
    !
    module.exports = function(grunt) {
    grunt.registerTask("hello",function(){
    grunt.log.writeln("Hello!");
    });
    };

    View full-size slide

  88. Easy peasy
    !
    $ grunt hello
    !

    View full-size slide

  89. Easy peasy
    !
    $ grunt hello
    !
    Running "hello" task
    Hello!
    !
    Done, without errors.

    View full-size slide

  90. grunt-contrib-clean
    grunt-contrib-coffee
    grunt-contrib-concat
    grunt-contrib-copy
    grunt-contrib-handlebars
    grunt-contrib-jshint
    grunt-contrib-jst
    grunt-contrib-less
    grunt-contrib-sass
    grunt-contrib-cssmin
    grunt-contrib-uglify
    grunt-watch-nospawn
    grunt-contrib-imagemin
    grunt-jasmine-bundle
    grunt-contrib-compass

    View full-size slide

  91. grunt-contrib-clean
    grunt-contrib-coffee
    grunt-contrib-concat
    grunt-contrib-copy
    grunt-contrib-handlebars
    grunt-contrib-jshint
    grunt-contrib-jst
    grunt-contrib-less
    grunt-contrib-sass
    grunt-contrib-cssmin
    grunt-contrib-uglify
    grunt-watch-nospawn
    grunt-contrib-imagemin
    grunt-jasmine-bundle
    grunt-contrib-compass
    grunt-*

    View full-size slide

  92. grunt-contrib-clean
    grunt-contrib-coffee
    grunt-contrib-concat
    grunt-contrib-copy
    grunt-contrib-handlebars
    grunt-contrib-jshint
    grunt-contrib-jst
    grunt-contrib-less
    grunt-contrib-sass
    grunt-contrib-cssmin
    grunt-contrib-uglify
    grunt-watch-nospawn
    grunt-contrib-imagemin
    grunt-jasmine-bundle
    grunt-contrib-compass
    grunt-*

    View full-size slide

  93. Client Server

    View full-size slide

  94. Client Server
    ?

    View full-size slide

  95. Client Server

    View full-size slide

  96. Client Server

    View full-size slide

  97. Lineman Sinatra

    View full-size slide

  98. Lineman Sinatra

    View full-size slide

  99. Lineman Sinatra

    View full-size slide

  100. Lineman Sinatra

    View full-size slide

  101. Lineman Sinatra

    View full-size slide

  102. Client Server

    View full-size slide

  103. Client Server

    View full-size slide

  104. Client Server

    View full-size slide

  105. Client Server

    View full-size slide

  106. Lineman Sinatra

    View full-size slide

  107. Lineman Sinatra

    View full-size slide

  108. Lineman Sinatra

    View full-size slide

  109. Lineman Sinatra

    View full-size slide

  110. 30 minute test build

    View full-size slide

  111. 4 minutes 4 minutes
    +

    View full-size slide

  112. 4 minutes 4 minutes
    +
    + 2 minutes

    View full-size slide

  113. 4 minutes 4 minutes
    +
    + 2 minutes
    10 minute test build

    View full-size slide

  114. It's habit forming.

    View full-size slide

  115. It's habit forming.

    View full-size slide

  116. A good start

    View full-size slide

  117. hi()
    code
    %
    save

    View full-size slide

  118. hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile

    View full-size slide

  119. hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    concat

    View full-size slide

  120. hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    concat
    hello
    world!
    play

    View full-size slide

  121. < 100ms
    hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    concat
    hello
    world!
    play

    View full-size slide

  122. < 100ms
    hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    concat
    hello
    world!
    play

    View full-size slide

  123. hi()
    code
    %
    save
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/
    tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    compile
    loadTask task for task in npmTasks
    grunt.renameTask "copy", "images"
    loadTask "grunt-contrib-copy" # load again so webfonts can use
    it
    grunt.renameTask "copy", "webfonts"
    loadTask "grunt-contrib-copy" # load again to make available
    in userland
    loadTask = (module) ->
    if fs.existsSync("#{process.cwd()}/node_modules/#{module}")
    grunt.loadNpmTasks(module)
    else
    grunt.loadTasks("#{__dirname}/../node_modules/#{module}/tasks")
    npmTasks = grunt.util._(linemanNpmTasks).chain().
    union("grunt-contrib-sass" if config.enableSass).
    union(config.loadNpmTasks).
    compact().value()
    concat test
    < 100ms

    View full-size slide

  124. Can your server
    host static files?

    View full-size slide

  125. !
    $ lineman build
    !

    View full-size slide

  126. !
    $ lineman build
    !
    $ tree dist

    View full-size slide

  127. !
    $ lineman build
    !
    $ tree dist
    !
    dist
    ├── css
    │ └── app.css
    ├── index.html
    └── js
    └── app.js

    View full-size slide

  128. !
    $ heroku config:set BUILDPACK_URL=http://github.com/
    testdouble/heroku-buildpack-lineman.git
    !
    $ git push
    Heroku

    View full-size slide

  129. !
    $ heroku config:set BUILDPACK_URL=http://github.com/
    testdouble/heroku-buildpack-lineman.git
    !
    $ git push
    Heroku
    Builds with Node.js

    View full-size slide

  130. !
    $ heroku config:set BUILDPACK_URL=http://github.com/
    testdouble/heroku-buildpack-lineman.git
    !
    $ git push
    Heroku
    Builds with Node.js
    Runs without Node.js

    View full-size slide

  131. Other Starter Projects

    View full-size slide

  132. angular
    Other Starter Projects

    View full-size slide

  133. backbone
    angular
    Other Starter Projects

    View full-size slide

  134. ember
    backbone
    angular
    Other Starter Projects

    View full-size slide

  135. ember
    backbone
    angular
    web libs
    Other Starter Projects

    View full-size slide

  136. ember
    backbone
    angular
    web libs markdown
    blogs
    Other Starter Projects

    View full-size slide

  137. Eminently extensible

    View full-size slide

  138. Add a dependency
    !
    $ npm install --save-dev grunt-typescript
    !

    View full-size slide

  139. Add a dependency
    !
    $ npm install --save-dev grunt-typescript
    !
    $ cat package.json

    View full-size slide

  140. Add a dependency
    !
    $ npm install --save-dev grunt-typescript
    !
    $ cat package.json
    ...
    "devDependencies": {
    ...
    "grunt-typescript": "0.1.6"
    }

    View full-size slide

  141. !
    loadNpmTasks: ['grunt-typescript'],
    config/application.js

    View full-size slide

  142. !
    loadNpmTasks: ['grunt-typescript'],
    !
    prependTasks: {
    common: ['typescript']
    },
    config/application.js

    View full-size slide

  143. !
    loadNpmTasks: ['grunt-typescript'],
    !
    prependTasks: {
    common: ['typescript']
    },
    !
    typescript: {
    compile: {
    src: 'app/js/**/*.ts',
    dest: 'generated/js/app.ts.js'
    }
    }
    config/application.js

    View full-size slide

  144. !
    $ lineman run
    !
    ...
    Running "typescript:compile" (typescript) task
    js: 0 files, map: 0 files, declaration: 0 files
    ...
    !
    Run the task

    View full-size slide

  145. JavaScript made easy.

    View full-size slide

  146. My name is Justin Searls
    Please tweet me @searls &
    Say [email protected]

    View full-size slide

  147. noun project attribution
    Yarn designed by Marie Coons from
    The Noun Project!
    !
    Scale designed by Ritika Khasgiwale
    from The Noun Project!
    !
    Business Man designed by Piotrek
    Chuchla from The Noun Project!
    !
    Business Man designed by Toke Thieden
    from The Noun Project!
    !
    Airplane designed by Sven Gabriel from
    The Noun Project!
    !
    Brick Wall designed by Juan Pablo Bravo
    from The Noun Project

    View full-size slide