Project 1 – Basic app with animated transitions

In this project, we are going to build a designer portfolio. There is stylish navigation menu, animated transitions and static photos viewer.

Screenshot of the app example
Screenshot of the app example

I have recorded a video of the app example that shows the animated transition. You may take a look to have a better understandings on what we are building.

http://mak.la/cjs-demo1

Mission Checklist

We are going to build the portfolio project by following these tasks.

  1. Setting up the project and GulpJS.
  2. Setting up the canvas and CreateJS library.
  3. Defining scene as container inheritance.
  4. Adding static stylish menu.
  5. Displaying another scene after menu selection.
  6. Animating transition between scenes.
  7. Optimizing the rendering for retina display.

Project Preparation

Throughout the project, we need several graphic files to complete the project. Please download the files via the following link.

http://mak.la/cjs-proj1.zip

After you downloaded the file, you will see the following files in the bundle.

PNG files for project 1
PNG files for project 1
images/
images/header.png
...
scripts/transitions.js
graphics_src/transitions.fla    

The transitions.js file is the animated transition exported from the Adobe Flash. If you don’t have the Adobe Flash, you can just use this file who comes with 2 transitions to follow the example. If you have Flash, however, you can open the transitions.fla file and customize the transition animation. You may even create new transition effects. We will talk about this later.

1. Setting up the project and GulpJS

In this step, we prepare the compiling environment for our project.

Preparation

Make sure we have nodejs and npm installed. npm is the package modules manager for nodejs. It’s included inside nodejs installer package.

Time for Action—Setting up the project folder

In this steps, we will initial our project with the GulpJS compiling automation setup.

  1. First, let’s create a folder for all the files in this project. The following is the initial file structure setup.
    Gulpfile.js
    Gulpfile.coffee
    package.json
    app/index.html
    app/images/
    app/scripts/
    app/scripts-src/
    app/scripts-src/app.coffee
    app/scripts-src/setting.coffee
    app/styles/
    app/styles/app.css
    
  2. We can generate the package.json file via the npm init method. After we go through the npm init process, we can then install the plugins via the following shell command.
     $ npm install --save-dev gulp coffee-script gulp-coffee gulp-concat
    
  3. After step 2, node.js should have written the following content into the package.json file. Check if you get similar result.
    package.json
     1 {
     2   "name": "JackPortfolio",
     3   "version": "1.0.0",
     4   "description": "An example for my book Rich interactive app development with C\
     5 reateJS",
     6   "main": "Gulpfile.js",
     7   "scripts": {
     8     "test": "echo \"Error: no test specified\" && exit 1"
     9   },
    10   "author": "Makzan",
    11   "license": "MIT",
    12   "devDependencies": {
    13     "coffee-script": "^1.8.0",
    14     "gulp": "^3.8.9",
    15     "gulp-coffee": "^2.2.0",
    16     "gulp-concat": "^2.4.1"
    17   }
    18 }
    
  4. Gulp always looks for the Gulpfile.js file when we execute the gulp tasks. If we want to write the Gulp tasks in CoffeeScript syntax, we need to load the CoffeeScript compiler in the Gulpfile.js and include the .coffee version of the GulpFile.
    gulp.js
    1 require('coffee-script/register');
    2 require('./Gulpfile.coffee');
    
  5. Now we can write our Gulp pipeline in CoffeeScript. The following configuration defines how the compiler should compile our source files. It also defined a watch task that watch any changes of .coffee file and go through the js task automatically.
    gulp.coffee
     1 gulp    = require 'gulp'
     2 coffee  = require 'gulp-coffee'
     3 concat  = require 'gulp-concat'
     4 
     5 gulp.task 'js', ->
     6   gulp.src [
     7     'app/scripts-src/setting.coffee'
     8     'app/scripts-src/app.coffee'
     9   ]
    10   .pipe coffee()
    11   .pipe concat 'app.js'
    12   .pipe gulp.dest 'app/scripts/'
    13 
    14 
    15 gulp.task 'watch', ->
    16   gulp.watch 'app/scripts-src/**/*.coffee', ['js']
    17 
    18 gulp.task 'default', ['js', 'watch']
    
  6. We create 2 CoffeeScript files to see if our Gulpfile works. They are app.coffee and setting.coffee. We will add real code logic into these files in next step.
    app.coffee
    1 console.log "App – Testing GulpJS setup."
    
    setting.coffee
    1 console.log "Setting – Testing GulpJS setup."
    
  7. Now we can run gulp in the terminal:
    1  $ ./node_modules/.bin/gulp 	
    
  8. We should see an app.js file is generated with the following content.
    app.js
    1 (function() {
    2   console.log("Setting – Testing GulpJS setup");
    3 
    4 }).call(this);
    5 
    6 (function() {
    7   console.log("App – Testing GulpJS setup.");
    8 
    9 }).call(this);
    

If you get the same result, that means our setup works.

What just happened?

We just set up the assets compiling tool chain. After setting up the building streamline, we are ready to dig into the early project development in next task.

NPM init

The development environment uses Node.js, although the project is for web browser. We need to setup the project folder with an npm—Node.js Package Manager. We can execute npm init and following the steps to generate a package.json file in the project directory.

After created the package.json file, we called the npm install.

1 $ npm install --save-dev gulp coffee-script gulp-coffee gulp-concat

The command installs the required package to compile our source code into JavaScript file. We used --save-dev which records the provided Node.js package as dependency libraries inside the package.json.

2. Setting up the canvas and CreateJS library

In this step, we setup the canvas and the CreateJS for the project.

Preparation

We need the CreateJS library. The easiest way to include the CreateJS is via the distribution of content delivery network.

http://code.createjs.com/

Optionally, we can download the code from the CreateJS github repository and host the files ourselves.

https://github.com/createjs

Time for Action—Setting up the Canvas and CreateJS Stage

Let’s follow the following steps to setup our canvas and CreateJS library.

  1. In the index.html, we prepare the basic HTML structure.
    index.html
     1 <!DOCTYPE html>
     2 <html lang="en">
     3   <head>
     4     <meta charset="utf-8">
     5     <meta name="viewport" content="width=device-width, initial-scale=1">
     6     <meta name="apple-mobile-web-app-capable" content="yes">
     7     <title>Jack Portfolio</title>
     8     <link rel="stylesheet" href="styles/app.css">
     9   </head>
    10   <body>
    11     <!-- The app element -->
    12     <div id="app">
    13       <canvas id="app-canvas" width="300" height="400"></canvas>
    14     </div>
    15 
    16     <!-- We load the JavaScript after content -->
    17     <script src="http://code.createjs.com/easeljs-0.7.1.min.js"></script>
    18     <script src="http://code.createjs.com/tweenjs-0.5.1.min.js"></script>
    19     <script src="http://code.createjs.com/movieclip-0.7.1.min.js"></script>
    20     <script src="scripts/app.js"></script>
    21   </body>
    22 </html>
    
  2. We have minimal styling in this task because our focus is on the canvas element. Add the following CSS to the styles/app.css file.
    styles/app.css
     1 /* Ensure the box sizing is the modern one. */
     2 * {
     3   box-sizing: border-box;
     4 }
     5 
     6 /* Basic reset */
     7 body {
     8   margin: 0;
     9   padding: 0;
    10 }
    11 
    12 /* canvases sit inside the #app frame. It’s similar to layers. */
    13 #app > canvas {
    14   position: absolute;
    15   top: 50%;
    16   left: 50%;
    17   height: 400px;
    18   width: 300px;
    19   margin-top: -200px;
    20   margin-left: -150px;
    21 }
    
  3. We created a file named setting.coffee which holds our global app configuration variables. Add the following width and height setting to the file.
    setting.coffee
    1 # a global app object.
    2 this.exampleApp ?= {}
    3 
    4 # Configurations
    5 this.exampleApp.setting = {
    6   width: 300
    7   height: 400
    8 }
    
  4. Then we create the entry point of our app in the app.coffee. Add the following code to the file.
    app.coffee
     1 # a global app object.
     2 this.exampleApp ?= {}
     3 
     4 cjs = createjs
     5 setting = this.exampleApp.setting
     6 
     7 class App
     8   # Entry point.
     9   constructor: ->
    10     console.log "Welcome to my portfolio."
    11     @canvas = document.getElementById("app-canvas")
    12     @stage = new cjs.Stage(@canvas)
    13 
    14     cjs.Ticker.setFPS 60
    15 
    16     # make sure the stage refresh drawing for every frame.
    17     cjs.Ticker.addEventListener "tick", @stage
    18 
    19 
    20 # Start the app
    21 new App()
    
  5. We have created the app’s foundation. Although we don’t see any content yet, the app foundation is ready and we can add our scene to the app in next step.

What just happened?

We just created the basic canvas app and CreateJS setup. In next step, we’ll build our scene. Let’s take a look at each part of code in this step.

Viewport
<meta name="viewport" content="width=device-width, initial-scale=1">

We target the app to be a mobile application. So we need to set a viewport. Mobile web browser simulate the device width as a desktop monitor to provide a better viewing experience for most desktop-only website. Viewport lets web designer tells the mobile browser the display configurations we want.

The default viewport of mobile web browser is about 980px. If we have created the styles dedicated to narrow screen, such as 320px width, we should change the viewport width to reflect the real device width.

Web app capable
<meta name="apple-mobile-web-app-capable" content="yes">

We want to provide an app experience to the user. When user add the web app into home screen, normal web pages act as bookmark. Tapping on them launch the mobile safari. After we set the apple-mobile-web-app-capable, the home screen bookmark acts like a real app. It has its own we view without the Safari user interface. It also has its own app switching screen in the multitask screen when user clicked the home button twice.

Default value when variable is undefined
this.exampleApp ?= {}

The equivalent way in JavaScript is:

if (this.exampleApp == null) this.exampleApp = {}

We can also express the same meaning with the following line, which looks cleaner.

this.exampleApp = this.exampleApp || {}
Centering the canvas

The canvas has fixed dimension. We can use the following styles to center aligning the canvas at the middle of the page.

1 #app > canvas {
2   position: absolute;
3   top: 50%;
4   left: 50%;
5   height: 400px;
6   width: 300px;
7   margin-top: -200px;
8   margin-left: -150px;
9 }

The code is inspired from the following CSS-Tricks website which shares different styling approaches to center elements.

http://css-tricks.com/centering-css-complete-guide/

CreateJS class

The class in CreateJS allows us to define a class definition and then we can create instance via the new method.

1 class App
2   # Entry point.
3   constructor: ->
4 
5 # Start the app
6 new App()

The simplicity of the CoffeeScript is it allows us to define and extends new classes.

Let’s take a look at the JavaScript from the CoffeeScript generator.

 1 var App;
 2 
 3 App = (function() {
 4   function App() {}
 5 
 6   return App;
 7 
 8 })();
 9 
10 new App();

In JavaScript, we can create a new object instance by using new on a defined function.

3. Defining scene as container inheritance

In this step, we define the Scene class which every page view builds on top of it.

Time for Action

Let’s follow the steps to define a Scene class.

  1. We create a dedicated file for the scenes definition. Add the following code to the scenes.coffee.
    scenes.coffee
     1  # a global app object.
     2  this.exampleApp ?= {}
     3 	
     4  # alias
     5  cjs = createjs
     6  setting = this.exampleApp.setting
     7 	
     8  class Scene extends cjs.Container
     9    constructor: (bgColor='blue')->
    10      # CreateJS super constructor
    11      @initialize()
    12 	
    13      # Draw a shape as the background color
    14      if bgColor != undefined
    15        shape = new cjs.Shape()
    16        shape.graphics
    17          .beginFill bgColor
    18          .drawRect 0, 0, setting.width, setting.height
    19 	
    20        # Add the shape to the display list, via using addChild
    21        @addChild shape
    22 	
    23  # export to global app scope      
    24  this.exampleApp.Scene = Scene
    
  2. In the app.coffee, we create the Scene instance and add it to the stage. This is a testing scene, we are going to change it to the real scene in next step. {title=”app.coffee”} Scene = this.exampleApp.Scene
    1  class App      
    2    constructor: ->
    3      ...
    4      # Temporary testing scene
    5      testScene = new Scene('gold')
    6      @stage.addChild testScene
    
  3. We have created a new file scenes.coffee, we need to include it into the GulpJS pipeline. Add the file into the gulp.src array.
    Gulpfile.coffee
    1  gulp.task 'js', ->
    2    gulp.src [
    3      './app/scripts/setting.coffee'
    4      './app/scripts/scenes.coffee'
    5      './app/scripts/app.coffee'
    6    ]
    7    .pipe coffee()
    8    .pipe concat 'app.js'
    9    .pipe gulp.dest './app/scripts/'
    

What just happened?

We have defined a Scene class and added a testing scene object to the stage.

Vector shape drawing

A shape is vector graphic that we express in mathematics. It’s like giving instruction on what the shape should look like.

1 shape = new cjs.Shape()
2 shape.graphics
3   .beginFill "white"
4   .drawRect 0, 0, 100, 50  

For every created display object, we need to add it to the display list. The following code assumes that we are adding the shape to a container.

@addChild shape    

If we are adding the shape to the stage, we can call the stage.addChild because the stage is a container.

Class inheritance in CoffeeScript

The CoffeeScript inheritance took us 3 lines to inherit class.

1 class Scene extends cjs.Container
2   constructor: (bgColor='blue')->        
3     @initialize()

In the generated code. It would take 13 lines of code in JavaScript, not including the _extends helper function.

 1 var Scene,
 2   __hasProp = {}.hasOwnProperty,
 3   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.\
 4 call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructo\
 5 r = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); ch\
 6 ild.__super__ = parent.prototype; return child; };
 7 
 8 Scene = (function(_super) {
 9   __extends(Scene, _super);
10 
11   function Scene(bgColor) {
12     if (bgColor == null) {
13       bgColor = 'blue';
14     }
15     this.initialize();
16   }
17 
18   return Scene;
19 
20 })(cjs.Container);

Actually, all the CreateJS follows its own way to create the object inheritance structure. The following source code of the Shape class shows how CreateJS inherits. {title=”Shape.js, from CreateJS”} (function() { “use strict”;

 1   var Shape = function(graphics) {
 2     this.initialize(graphics);
 3   };
 4   var p = Shape.prototype = new createjs.DisplayObject();
 5   Shape.prototype.constructor = Shape;
 6 
 7   // public properties:
 8 
 9     p.graphics = null;
10 
11   // constructor:
12 
13     p.DisplayObject_initialize = p.initialize;
14 
15     p.initialize = function(graphics) {
16       this.DisplayObject_initialize();
17       this.graphics = graphics ? graphics : new createjs.Graphics();
18     };
19 
20 
21     p.isVisible = function() {
22       ...
23     };
24 
25 
26     p.DisplayObject_draw = p.draw;
27 
28 
29     p.draw = function(ctx, ignoreCache) {
30       ...
31     };
32 
33 
34     p.clone = function(recursive) {
35       ...
36     };
37 
38     p.toString = function() {
39       ...
40     };
41 
42   createjs.Shape = Shape;
43 }());

Exporting the class definition

We separate each part of code into its own file. The benefit of having separated files is that we can modularity logic into very specific domain. For every specific module, we only focus on its own logic. This helps making each parts less bugs.

It is a good practice that each file is separated. The compiled JavaScript of each files are put into an isolated function group by default. If we need to expose specific variables to other files, we can reference them to the global object under the app namespace.

this.exampleApp.Scene = Scene

Then we can reference the exported Class in another file.

Scene = this.exampleApp.Scene

4. Adding static stylish menu

In this step, we implement the menu scene and put menu item on it.

Time for Action

Let’s follow the steps to create the menu scene.

  1. We create the our menu scene. Let’s add the code to scenes.coffee.
    scenes.coffee
     1  class SceneA extends Scene
     2    constructor: ->
     3      super('#EDE4D1')
     4 	
     5      header = new cjs.Bitmap 'images/header.png'
     6      header.scaleX = header.scaleY = 0.5
     7      @addChild header
     8 	
     9      info = new cjs.Bitmap 'images/info.png'
    10      info.y = 356
    11      info.scaleX = info.scaleY = 0.5
    12      @addChild info
    13 	
    14      photoA = new cjs.Bitmap 'images/a.png'
    15      photoA.y = 38
    16      photoA.scaleX = photoA.scaleY = 0.5
    17      @addChild photoA
    18 	
    19      photoB = new cjs.Bitmap 'images/b.png'
    20      photoB.y = 146
    21      photoB.scaleX = photoB.scaleY = 0.5
    22      @addChild photoB
    23 	
    24      photoC = new cjs.Bitmap 'images/c.png'
    25      photoC.y = 253
    26      photoC.scaleX = photoC.scaleY = 0.5
    27      @addChild photoC
    28 	
    29  # export to global app scope      
    30  this.exampleApp.SceneA = SceneA    
    
  2. Make sure we import any newly created class into our App scope in order to use them.
    app.coffee
    1  # alias
    2  cjs = createjs
    3  setting = this.exampleApp.setting    
    4  Scene = this.exampleApp.Scene
    5  SceneA = this.exampleApp.SceneA
    
  3. In the app logic, we replace the old Scene by the newly created SceneA class.
    app.coffee
    1  class App  
    2    constructor: ->
    3      ...
    4      sceneA = new SceneA()
    5      @stage.addChild sceneA
    

What just happened?

We created a new scene by inheriting the original Scene class definition. The inheritance allows us to define custom scene easily.

5. Displaying another scene after menu selection

In this step, we build a simple scene manager to control the presence of different scenes.

Preparation

Our scene management is inspired from the navigation controller in iOS. The navigation controller stores a stack of added scene. Developers that use this manager can push and pop scenes.

Time for Action

Let’s follow the steps to create our own scene manager for the app.

  1. We have more than 1 scene in our app. To make things easier, we design a scene manager that manage the scene displaying and leaving. we create a new file named scene-management.coffee for this logic. Then put the following code into the newly created file.
    scene-management.coffee
     1  # a global app object.
     2  this.exampleApp ?= {}
     3 	
     4  # An object to manage scene, under the app namespace.
     5  this.exampleApp.sceneManager = {
     6    stage: undefined
     7    scenes: []
     8    lastScene: -> @scenes[@scenes.length-1]
     9    resetWithScene: (scene) ->
    10      @scenes.length = 0
    11      @scenes.push scene
    12      @stage.addChild scene
    13    popScene: ->
    14      @stage.removeChild @lastScene()
    15      @scenes.pop()
    16      @lastScene().mouseEnabled = true
    17    pushScene: (scene)->
    18      @lastScene().mouseEnabled = false
    19      @scenes.push scene
    20      @stage.addChild scene
    21  }
    
  2. We create more scenes to test our example. Add the SceneB to the scenes.coffee.
    scenes.coffee
     1  class SceneB extends Scene
     2    constructor: (contentId='a')->
     3      super('white')
     4 	
     5      content = new cjs.Bitmap "images/page-view-content-#{contentId}.png"
     6      content.scaleX = content.scaleY = 0.5
     7      @addChild content
     8 	
     9      header = new cjs.Bitmap 'images/header-back.png'
    10      header.scaleX = header.scaleY = 0.5
    11      @addChild header
    12 	
    13      header.on 'click', ->
    14        sceneManager.popScene()
    
  3. Then we create the SceneInfo.
    scenes.coffee
     1  class SceneInfo extends Scene
     2    constructor: ->
     3      super('white')
     4 	
     5      content = new cjs.Bitmap "images/info-content.png"
     6      content.scaleX = content.scaleY = 0.5
     7      @addChild content
     8 	
     9      @on 'click', ->
    10        sceneManager.popScene()          
    
  4. Make sure we export the newly defined class so that the App, which is in another file, can access to these classes.
    app.coffee
    1  # export to global app scope      
    2  this.exampleApp.SceneA = SceneA
    3  this.exampleApp.SceneB = SceneB
    4  this.exampleApp.SceneInfo = SceneInfo   
    
  5. In the scenes.coffee file, we add the click event handling to the menu elements. Tapping the elements will lead to a new scene to display the image or the information scene.
    scenes.coffee
     1  sceneManager = this.exampleApp.sceneManager
     2 	
     3  info = new cjs.Bitmap 'images/info.png'
     4  info.y = 356
     5  info.scaleX = info.scaleY = 0.5
     6  @addChild info
     7  info.on 'click', ->
     8    scene = new SceneInfo()
     9    sceneManager.pushScene scene
    10 	
    11  # Menu item 1
    12  photoA = new cjs.Bitmap 'images/a.png'
    13  photoA.y = 38
    14  photoA.scaleX = photoA.scaleY = 0.5
    15  @addChild photoA
    16  photoA.on 'click', ->
    17    scene = new SceneB('a')
    18    sceneManager.pushScene scene
    19 	
    20  # Menu item 2
    21  photoB = new cjs.Bitmap 'images/b.png'
    22  photoB.y = 146
    23  photoB.scaleX = photoB.scaleY = 0.5
    24  @addChild photoB
    25  photoB.on 'click', ->
    26    scene = new SceneB('b')
    27    sceneManager.pushScene scene
    28 	
    29  # Menu item 3
    30  photoC = new cjs.Bitmap 'images/c.png'
    31  photoC.y = 253
    32  photoC.scaleX = photoC.scaleY = 0.5
    33  @addChild photoC
    34  photoC.on 'click', ->
    35    scene = new SceneB('c')
    36    sceneManager.pushScene scene
    
  6. We have created a few new scenes. Make sure we have aliased these new classes in the app.coffee file.
    app.coffee
    1  # alias
    2  cjs = createjs
    3  setting = this.exampleApp.setting
    4  sceneManager = this.exampleApp.sceneManager
    5  SceneA = this.exampleApp.SceneA
    6  SceneB = this.exampleApp.SceneB
    7  SceneInfo = this.exampleApp.SceneInfo
    
  7. In the main App logic, We removed the old Scene creation logic and make use of the sceneManager to handle the scene visualization.
    app.coffee
     1  class App      
     2    constructor: ->
     3      ...
     4 	
     5      sceneA = new SceneA()
     6      @stage.addChild sceneA
     7 	
     8      sceneManager.stage = @stage
     9 	
    10      scene = new SceneA()
    11      sceneManager.resetWithScene scene
    
  8. We created new files so we need to include the files in the Gulpfile compiling pipeline.
    Gulpfile.coffee
     1  gulp.task 'js', ->
     2    gulp.src [
     3      './app/scripts/setting.coffee'
     4      './app/scripts/scene-manager.coffee'
     5      './app/scripts/scenes.coffee'
     6      './app/scripts/app.coffee'
     7    ]
     8    .pipe coffee()
     9    .pipe concat 'app.js'
    10    .pipe gulp.dest './app/scripts/'
    

What just happened?

The scene manager is an object without class definition. We put it on the exampleApp namespace to let other modules access it.

There are 2 properties, stage and scenes. The stage is refer to the target container that holds the scenes. The scenes is an array of the scenes we have added to the stage.

Then we defined 3 essential methods, resetScene, pushScene and popScene, and 1 helper method, lastScene.

The resetWithScene clears the scenes array to provide a clean state. Then it add the give scene as the first scene, as known as root scene in such kind of navigation pattern.

The pushScene takes the given new scene object and add to the scenes stack. Then it displays the new added scene to the screen.

The popScene, on the other hand, remove the last scene from the screen and from the scenes stack. That’s why we have a helper method that returns the last scene.

6. Animating transition between scenes

In this step, we make use of the exported Flash animation to build the animated transition effect.

Preparation

Before we begin, make sure we have the transitions.js file ready in the scripts folder. We include the file into the index.html before loading our main App logic.

index.html
1 ...
2   <script src="http://code.createjs.com/easeljs-0.7.1.min.js"></script>
3   <script src="http://code.createjs.com/tweenjs-0.5.1.min.js"></script>
4   <script src="http://code.createjs.com/movieclip-0.7.1.min.js"></script>
5   <script src="scripts/transitions.js"></script>
6   <script src="scripts/app.js"></script>
7 </body>

Time for Action

Let’s work on the following steps to add the animated transition to the app.

  1. In the scene-manager.coffee, we add one new method pushSceneWithTransition which add the animated transition while switching scenes.
    scene-manager.coffee
     1  this.exampleApp.sceneManager = {
     2    ...
     3    pushSceneWithTransition: (scene, transitionClassName) ->
     4      transition = new lib[transitionClassName]()
     5      transition.x = setting.width/2
     6      transition.y = setting.height/2
     7 	
     8      scene.visible = false
     9 	
    10      @pushScene scene
    11 	
    12      # The transition animation in Flash should dispatch `sceneShouldChange` eve\
    13 nt.
    14      transition.on 'sceneShouldChange', ->
    15        scene.visible = true
    16 	
    17      @stage.addChild transition
    18  }
    
  2. In the scenes.coffee, we change to use the new pushSceneWithTransition method.
    scenes.coffee
     1  class SceneA extends Scene
     2    constructor: ->
     3 	
     4  ...
     5  info.on 'click', ->
     6    scene = new SceneInfo()
     7    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationA'
     8 	
     9  ...
    10  photoA.on 'click', ->
    11    scene = new SceneB('a')
    12    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    13 	
    14  ...
    15  photoB.on 'click', ->
    16    scene = new SceneB('b')
    17    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    18 	
    19  ...
    20  photoC.on 'click', ->
    21    scene = new SceneB('c')
    22    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    

What just happened?

We have added a custom animated transition when we switch scene in the app.

Adding the generated transition

Any exported Flash movieclip is put into a lib namespace. For example, if the movieclip name is AnimatedBall, we can create an instance by using new lib.AnimatedBall().

In our code, the transition class name is a variable. By using the array notation instead of dot notation, we can create new instance of a class where the class name is variable.

new lib[transitionClassName]()
Custom event: sceneShouldChange

In the scene manager, we listen to the sceneShouldChange event and toggle the new scene’s visibility.

1 transition.on 'sceneShouldChange', ->
2   scene.visible = true

This relies on the Flash animation which dispatches the event at the middle of the transition animation.

sceneShouldChange event in the Flash timeline
sceneShouldChange event in the Flash timeline

In the screenshot, you will find an action is defined in the middle of the transition animation. When the animation reaches this frame, it dispatch the event. We capture this custom event in the scene manager to actually switch the scene.

7. Optimizing for retina display

We may find the app looks blurry when we test the web app in iPhone or Android device with high-definition display. That’s because the retina display trys to render the graphics by doubling our pixels. In this step, we optimize the canvas rendering in retina display.

Time for Action

Let’s add the retinalize utility via the following steps.

  1. The retinalize method is kind of utility that’s independent to our logic. We create a new file utility.coffee and place the following code inside it.
    utility.coffee
     1  retinalize = (canvas, stage) ->
     2    # We skip the logic if the device is not retina
     3    # or it doesn’t support the pixel ratio
     4    return if (window.devicePixelRatio)
     5 	
     6    # cache the pixel ratio 
     7    ratio = window.devicePixelRatio      
     8    # get the original canvas dimension
     9    height = canvas.getAttribute('height')
    10    width = canvas.getAttribute('width')
    11 	
    12    # set the new dimension with ratio multiplication
    13    canvas.setAttribute('width', Math.round(width * ratio))
    14    canvas.setAttribute('height', Math.round( height * ratio))
    15 	
    16    # ensure the canvas CSS style follows the original dimension
    17    canvas.style.width = width+"px"
    18    canvas.style.height = height+"px"
    19 	
    20    # scale the entire stage so we can use the original coordinate in our app.
    21    stage.scaleX = stage.scaleY = ratio
    
  2. We can then call the retinalize method after we initialize the canvas and stage variable.
    app.coffee
    1  class App
    2    # Entry point.
    3    constructor: ->
    4      console.log "Welcome to my portfolio."
    5      @canvas = document.getElementById("app-canvas")
    6      @stage = new cjs.Stage(@canvas)
    7 	
    8      window.utility.retinalize(@canvas, @stage)
    
  3. We have created a new file. As usual, we include the new file in our compiling pipeline. Add the following highlighted line to the Gulpfile.coffee.
    Gulpfile.coffee
     1  gulp.task 'js', ->
     2    gulp.src [
     3      './app/scripts/setting.coffee'
     4      './app/scripts/retinalize.coffee'
     5      './app/scripts/scene-manager.coffee'
     6      './app/scripts/scenes.coffee'
     7      './app/scripts/app.coffee'
     8    ]
     9    .pipe coffee()
    10    .pipe concat 'app.js'
    11    .pipe gulp.dest './app/scripts/'
    

What just happened?

When the browser detects the display has a higher devicePixelRatio, which means for every ‘point’ of the display, it renders more than 1 pixels. For such types of display, we enlarge the canvas content while keeping the dimension of the <canvas> element unchanged. This allows the retina display to render the graphics in its native pixel resolution, and hence make the canvas graphics looks sharp.

Further challenges

There are some essential features we haven’t implemented in the example app.

For example, we don’t have scrolling in the menu scene so we can’t display more photos.