Project 1B – DOM-based app with animated transitions

In this project, we improve the Project 1 to make all the content accessible by the browsers.

Mission Checklist

We are going to replace the canvas-based content into DOM elements.

  1. Defining DOM elements
  2. Controlling page-based scenes.
  3. Positioning the canvas-based transition to fit window size.
  4. Falling back for old browser without canvas support.
  5. Fine tuning the app styles.
  6. Supporting retina display.

Project Preparation

In the project, we will need the updated image assets. You may download them via the following URL.

http://mak.la/cjs-proj1b-images.zip

1. Defining DOM elements

Time for Action

  1. In our index.html we had the canvas inside the #app DIV. We will add new DOM elements after this canvas tag, for each page of content.
    index.html
    1  <div id="app">
    2    <canvas id="app-canvas" width="300" height="400"></canvas>
    3    <!-- We will add each page of content from here -->
    4  </div>
    
  2. First, we add the main menu page. Add the following #main DIV after our canvas.
    index.html
     1  <div id="app">
     2    <canvas id="app-canvas" width="300" height="400"></canvas>      
     3    <div id="main" class="page">
     4      <div class='header'>
     5        <img src="images/header.png" alt="Header">
     6      </div>
     7      <ul id="main-list" class='non-collapse-content'>
     8        <li><a href="#detail-page"><img src='images/a.png' alt='Photo A'></a></li>
     9        <li><a href="#detail-page"><img src='images/b.png' alt='Photo B'></a></li>
    10        <li><a href="#detail-page"><img src='images/c.png' alt='Photo C'></a></li>
    11        <!-- Feel free to add more list items here -->
    12      </ul>
    13      <div id="info-link">
    14        <a href="#info-page" data-transition="TransitionAnimationA"><img src='ima\
    15 ges/info.png' alt='Link to Info'></a>
    16      </div>
    17    </div>
    18  </div>
    
  3. Next, we add the #detail-page after the mail page. You may add other page if needed.
    index.html
     1  <div id="app">
     2    <canvas id="app-canvas" width="300" height="400"></canvas>
     3    <div id="main" class="page">...</div>
     4    <div id="detail-page" class="page">
     5      <a href="#" class='header'><img src='images/header-back.png' alt='Back to m\
     6 ain'></a>
     7      <img src='images/photo-a.png' alt='Detail of Photo A'>
     8      <p>Here is a photo from Unsplash. The photo is free for commercial use. I p\
     9 ut it here just for the app example. The photo was taken by Ben Moore.</p>
    10      <div id="info-link">
    11        <a href="#info-page" data-transition="TransitionAnimationA"><img src='ima\
    12 ges/info.png' alt='Link to Info'></a>
    13      </div>
    14    </div>
    15  </div>
    
  4. Finally, we add the #info-page. Please note that we have replaced the text image into real text.
    index.html
     1  <div id="app">
     2    <canvas id="app-canvas" width="300" height="400"></canvas>
     3    <div id="main" class="page">...</div>
     4    <div id="detail-page" class="page">...</div>
     5    <div id="info-page" class="page">
     6      <a href="#" class='header'><img src='images/header-back.png' alt='Back to m\
     7 ain'></a>
     8      <div class='non-collapse-content'>
     9        <p>This is an example app that serves as the 1st chapter of my book  Ric\
    10 h Interactive App Development with CreateJS. This example demonstrates a custom \
    11 animated transition. It lacks some essential features but this is just for the c\
    12 hapter 1. More features coming in future chapter.</p>
    13        <p>This example is bought to you by Makzan. He has written three books an\
    14 d one video course on building a Flash virtual world and creating games with HTM\
    15 L5 and the latest web standards. He is currently teaching courses in Hong Kong a\
    16 nd Macao SAR.</p>
    17      </div>
    18    </div>
    19  </div>
    
  5. Before we move on, we add some basic styles. It replaces the CSS file from project 1.
    app.css
     1  ul {
     2    list-style: none;
     3  }
     4 	
     5  img {
     6    width: 100%;
     7    border: 0;
     8  }
     9 	
    10  /* canvases sit inside the #app frame. It’s similar to layers. */
    11  #app {
    12    position: relative;
    13  }
    14  #app > canvas {
    15    position: fixed;
    16    display: none; /* default hide until we use it */
    17    z-index: 999;
    18  }
    

What just happened?

We are building the project of Jack Portfolio from scratch. We use DIV with .page class to indicate one page of content. They are all added into #app element.

Here is the new #app DOM structure.

1 <div id="app">
2   <canvas id="app-canvas" width="300" height="400"></canvas>
3   <div id="main" class="page">...</div>
4   <div id="detail-page" class="page">...</div>    
5   <div id="info-page" class="page">...</div>
6 </div>

The HTML is designed to be used without any JavaScript and fancy transition effect. All the links between content are based on the hash anchors. User can still view and link to different part of the content with neither canvas support nor JavaScript support.

2. Page Transition Manager

In this step, we control our .page DOM elements by a PageManager.

Preparation

We need to modify the scenes management. It was controlling the DisplayObject in CreateJS canvas. Now we need to control the DOM elements. I changed the class from SceneManager to PageManager to make the code less confusing. The structure will be the same as what we had in canvas-based scenes manager.

 1 class app.PageManager
 2   constructor: (@stage)->    
 3     # init the pages    
 4   lastScene: -> 
 5     # return last scene
 6   resetWithScene: (scene) ->
 7     # reset scene
 8   popScene: ->    
 9     # remove the last scene
10   pushScene: (scene)->
11     # show the given scene    
12   pushSceneWithTransition: (scene, transitionClassName) ->
13     # show the given scene with transition

Time for Action

  1. First, we work on the CSS. Each page is absolute positioned
    app.css
     1  /* canvases sit inside the #app frame. It’s similar to layers. */
     2  #app {
     3    position: relative;
     4  }
     5  #app > canvas {
     6    position: fixed;
     7    display: none; /* default hide until we use it */
     8    z-index: 999;
     9  }
    10 	
    11  /* Page related */
    12  .page {
    13    width: 100%;
    14    height: 100%;
    15    position: absolute;
    16  }
    
  2. By default, we hide all the .page. The first page will be added from the resetWithScene method.
    page-manager.coffee
     1  constructor: (@stage)->
     2      @scenes = []  
     3      $('.page').hide()
     4 	
     5      # register clicks on all pages            
     6      $('a[href^="#"]').click (event) =>
     7        pageId = $(event.currentTarget).attr('href')
     8 	
     9        # when it's link to #, it is a back transition
    10        pageId = '.page:first' if pageId == '#'
    11 	
    12        transition = $(event.currentTarget).data('transition')
    13 	
    14        @pushSceneWithTransition $(pageId), transition
    
  3. The lastScene returns the last element of the scenes array.
    page-manager.coffee
    1  lastScene: -> @scenes[@scenes.length-1]
    
  4. In the resetScene method, we reset the scenes array and use jQuery to show the given scene.
    page-manager.coffee
    1  resetWithScene: (scene) ->
    2    @scenes.length = 0
    3    @scenes.push scene
    4 	
    5    $(scene).show()  
    
  5. When we remove scene, we don’t actually remove the scene like we did in canvas. Instead, we hide the DOM element of the scene.
    page-manager.coffee
    1  popScene: ->    
    2    $(scene).hide()
    3    @scenes.pop()    
    
  6. In DOM elements, we control the DOM’s visibility. So pushing a scene means we hide the last one and show the given one. The browser may be scrolled in the last scene, that’s why we reset the scroll position.
    page-manager.coffee
    1  pushScene: (scene)->
    2      $(@lastScene()).hide()
    3      @scenes.push scene
    4      $(scene).show()
    5 	
    6      # reset the scroll            
    7      $(window).scrollTop(0)
    
  7. Here comes the core part, transition. The transition is still controlled by canvas. During the control of the DOM visibility, we show the canvas animation and hide the canvas after the transition completed.
    page-manager.coffee
     1    pushSceneWithTransition: (scene, transitionClassName='TransitionAnimationB') \
     2 ->
     3    transition = new lib[transitionClassName]()
     4 	
     5    # The demension follows the Flash canvas dimension
     6    transition.x = 300/2
     7    transition.y = 400/2    
     8    $('#app-canvas').show()
     9 	
    10    transition.on 'sceneShouldChange', =>
    11      @pushScene scene
    12 	
    13    transition.on 'transitionEnded', ->
    14      $('#app-canvas').hide()
    15 	
    16    @stage.addChild transition
    
  8. Let’s make use of the page transition. We change the app.coffee to the following code.
    app.coffee
     1  # a global app object.
     2  this.exampleApp ?= {}
     3 	
     4  # alias
     5  cjs = createjs
     6  setting = this.exampleApp.setting
     7  app = this.exampleApp
     8 	
     9  class App
    10    # Entry point.
    11    constructor: ->
    12      console.log "Welcome to my portfolio."
    13 	
    14      @canvas = document.getElementById("app-canvas")
    15      @stage = new cjs.Stage(@canvas)
    16 	
    17      cjs.Ticker.setFPS 60
    18      cjs.Ticker.addEventListener "tick", @stage # make sure the stage refresh dr\
    19 awing for every frame.
    20 	
    21      app.sceneManager = new app.PageManager(@stage)
    22      app.sceneManager.resetWithScene $('.page:first')
    23 	
    24  new App()
    

What just happened?

We created the page transition manager that is very similar to our previous scene manager. The only difference is that page manager handles .page DOM element and scene manager handles CreateJS container.

Improvement

We have reset the scroll position during scenes transition. In future, we may store the scroll position of each scene, so that we can resume the previous scroll position when popping to the last scene.

3. Positioning the canvas transition

Time for Action

  1. In the retinalize.coffee file, we add a setFullScreen function that scales the canvas to fit the window dimension.
    retinalize.coffee
     1  # a global app object.
     2  this.exampleApp ?= {}
     3 	
     4  setting = this.exampleApp.setting
     5 	
     6  this.utility ?= {}
     7 	
     8  this.utility.setFullScreen = (canvas, stage) ->
     9    canvas.setAttribute 'width', $(window).width()
    10    canvas.setAttribute 'height', $(window).height()
    11    setting.width = $(window).width()
    12    setting.height = $(window).height()
    13 	
    14    # 300 is the original Flash canvas width
    15    stage.scaleX = stage.scaleY = setting.width / 300 
    
  2. In our app’s entry point, we invoke the setFullScreen function for the first time and register it to be run every time when the window resizes.
    app.coffee
     1  class App
     2    # Entry point.
     3    constructor: ->
     4      console.log "Welcome to my portfolio."
     5 	
     6      @canvas = document.getElementById("app-canvas")
     7      @stage = new cjs.Stage(@canvas)
     8 	
     9      utility.setFullScreen(@canvas, @stage)
    10 	
    11      window.onresize = =>
    12        utility.setFullScreen(@canvas, @stage)
    13 	
    14      ...
    

4. Falling back in old browser

Time for Action

  1. We add a new file old-browser.js to fall back the logic to basic HTML anchors navigation when the reader’s browser doesn’t support Canvas.
    old-browser.js
     1  (function(){  
     2    // Check if canvas is supported
     3    isCanvas2DSupported = !!window.CanvasRenderingContext2D;
     4 	
     5    // Give up all logic 
     6    if(!isCanvas2DSupported) {
     7      // remove .page styles
     8      $('.page').removeClass();
     9    }          
    10  }).call(this);
    
  2. We include the old-browser.js file right after the loading of jQuery and before loading our logic.
    index.html
    1  <script src="scripts/old-browser.js"></script>
    

What’s happening?

We check if the browser supports the canvas. When the browser is too old to run canvas, we fall back to the step-1 which presents the content via browser hash link. This is done by removing all the .page class to force the scene transition and the .page styles not working.

5. Fine tuning the styles

Time for Action

  1. In the app.css, we add some more styles to make the app looks nice.
    app.css
     1  #info-link {  
     2    position: fixed;
     3    bottom: -5px;
     4    left: 0;
     5    width: 100%;
     6  }
     7 	
     8  .header {
     9    position: fixed;
    10    top: 0;
    11    left: 0;
    12    width: 100%;
    13  }
    14 	
    15  .non-collapse-content {
    16    margin-top: 25%;
    17  }
    18 	
    19  p {
    20    padding: 1rem;
    21  }
    22 	
    23  #main-list {
    24    padding-bottom: 100px;
    25  }
    26  #main-list li {
    27    margin-top: -11%;
    28  }
    

6. Supporting retina display

Preparation

Make sure you have downloaded the new images assets that contain the @2x version of the images.

http://mak.la/cjs-proj1b-images.zip

Time for Action

  1. In the index.html, We add the srcset attribute for every img tag.
    index.html
     1  <img src="images/header.png" srcset='images/header.png 1x, images/header@2x.png\
     2  2x' alt="Header">
     3  ...
     4  <li><a href="#detail-page"><img src='images/a.png' srcset='images/a.png 1x, ima\
     5 ges/a@2x.png 2x' alt='Photo A'></a></li>
     6  <li><a href="#detail-page"><img src='images/b.png' srcset='images/b.png 1x, ima\
     7 ges/b@2x.png 2x' alt='Photo B'></a></li>
     8  <li><a href="#detail-page"><img src='images/c.png' srcset='images/c.png 1x, ima\
     9 ges/c@2x.png 2x' alt='Photo C'></a></li>
    10  ...
    11  <img src='images/info.png' srcset='images/info.png 1x, images/info@2x.png 2x' a\
    12 lt='Link to Info'>
    13  ...
    14  <img src='images/header-back.png' srcset='images/header-back.png 1x, images/hea\
    15 der-back@2x.png 2x' alt='Back to main'>
    

What’s happening?

srcset allows us to define separated images sources for different screen density.

Here is the syntax.

1 <img src='DEFAULT_PATH' src='FILE_PATH 1x, FILE_PATH 2x, FILE_PATH 3x' alt=''>