Practical CreateJS – Building Rich Interactive Web App
Practical CreateJS – Building Rich Interactive Web App
Makzan
Buy on Leanpub

Table of Contents

About author

Makzan focuses on web development and game design. He has over 14 years of experience in building digital products. He has worked on real-time multiplayer interaction games, iOS applications, and rich interactive websites.

He has written 3 books and 1 video course on building a Flash virtual world and creating games with HTML5 and the latest web standards. He is currently teaching courses in Hong Kong and Macao SAR. He writes tutorials and shares his thoughts on makzan.net.

Books by Makzan

Makzan has published 3 books and 1 video course.

  • HTML5 game development hotshot – Step by step game projects
  • HTML5 game development video
  • HTML5 games development for beginners: A beginner’s guide
  • Flash multiplayer virtual world

Thanks

Writing book is never easy. I would like to thank my family to support my writings.

Also, great thanks to the following friends who keep giving feedback a during the book writing process.

  • Franklin Ng

Preface

This book focuses on building canvas-based application and games with the CreateJS suite. The gap between web application and native application is not as big as usual, thanks to the performance boost of the browser engine and the device processing power. By using web technologies in app development, we can benefit from the flexibility of web programming. Canvas and CreateJS allows us to go beyond static display. By combining the DOM elements and rich media rendering in canvas, we can provide immersive experience to our users.

Do keep in mind that we are never locked to any technology. Canvas based animation can works well together with the CSS animation and transition. We can also go for the hybrid approach by making use of the native navigation together with the high performance web view to render our content.

Hope you enjoy the book examples and go further by creating your rich interactive applications.

Code Example

You can find the code example in the following GitHub repository.

http://mak.la/cjs-examples

You can git clone the project to your machine so that you can update the code by using git commands. Alternatively, you may click “Download Zip” to download the whole bundle.

Download source code

Download source code

Preparation

In this part, we setup our environment and get ready for getting our hand dirty to code the example project.

Tools we need in this book

We need the following tools to go through our projects.

Operating system

Mac OS X is preferred to build iOS app. Otherwise, Linux and Windows works well as development machine.

Required software

  • Code editor

    Any text editor works. I recommend Sublime Text if you don’t have a preference.

  • Modern web browsers

    Browsers are needed to test and debug our web app rendering.

  • Android and iOS Simulator

    We need simulator to test how our web apps run in mobile operating system. We’ll use Genymotion or official simulator for Android. We need Mac OS X and Xcode for the iOS simulator.

Actually I always recommend testing our web apps in real devices. We can feel our own interface by holding it on hand. Is the button placement easy for tap? Is the font size suitable? How is the performance of the app running in aged devices? These are the questions that only real devices testing can answer.

Optional

  • Adobe Flash

    We will create some vector animations in Adobe Flash. CreateJS library integrates with the flash IDE so that we can export the timeline animation directly into the canvas. This is optional because the course code contains the exported animation we need. But if you want to change the animation, you may need Flash IDE to modify the source .fla file.

What version of Adobe Flash supported?

The exporting tool exists as a plugin in Adobe CS6. In CC 17.0, the tool appears as a command. In CC 17.1. The CreateJS toolkit is deeply integrated into the application. There are a new kind of .fla document called Canvas Fla. For any existing old fla project, we can simply convert the fla file to the new format by using the Command > Convert a normal fla to canvas fla.

It’s always worth backing up the file before huge changes, such as format convertion.

  • PhoneGap build service

    We need to pack the web pages and asset files into binary application in order to deploy it on app stores. PhoneGap is one of the solutions. The setup, however, may not be easy enough for web designer. PhoneGap Build provides a web service that lets us upload the client-side code and files. Then it builds the binary applications for different platforms without us handling the compilation environment.

PhoneGap displays the web app inside a web view and use a native wrapper to build it as binary app. PhoneGap provides more than just a web view. It has many native API access exposed to the JavaScript scope.

CoffeeScript compiling tools

When we write the logic in CoffeeScript, we need to compile them into JavaScript. You can choose your own method to compile the .coffee file into .js. Some compilers suggestion lists here in case you don’t have a favorite one yet.

  • GruntJS GruntJS is a command line task runner for JavaScript. It allows developer to define how some inputs can be manigulated and transformed into the destination files.
  • GulpJS GulpJS provides Grunt-simira task runner. The different is that GulpJS uses the concept of pipelane. That means we can write the tasks in cleaner way by ‘chaining’ the tasks.
  • LiveReload LiveReload is a GUI tool that provides several handy features. It auto refreshes the browser when you save you files. It also compiles the preprocessor files for you, including CoffeeScript, SCSS and more.
  • CodeKit CodeKit is another GUI tool with similar features from LiveReload. It is mac only and provides shortcut to install commonly-used libraries into your projects.

Pre-Request

This book assumes you have basic knowledge of client-side web development techniques. In order to follow the examples, you need to know basic HTML and CSS. You’ll learn some intermediate JavaScript programming skills too, but knowing some fundamental JS would be great.

Searching online is helpful when you have questions on web issues. If you need reference on using the web standards, I recommend checking the Mozilla Developer Network for reference and StackOverflow for questions and issues.

The following is a list of online resource that you may learn yourself alongside the book examples.

HTML and CSS

Here is a list of resources if you need to get started learning HTML and CSS, the browser technology that display information and formatting styles.

  • HTML and CSS book by Jon Duckett.

    This book helps you learn the most basic web design skills from scratch. The book demonstrates web design technologies with useful graphs and code examples.

  • Learn to Code HTML & CSS by Shay Howe.

    Shay Howe shows the essential techniques to craft your own web page. It’s easy to follow and you’ll learn the fundamentals to get started as web designer.

  • Learn CSS Layout by Bocoup.

    There are many ways to lay out content in CSS. This website provides comprehensive reference on all major layout approaches.

  • Learn to Code Advanced HTML & CSS by Shay Howe.

    After you get familiar with basic HTML and CSS, you may read this resource that will take you to the next level of web design.

JavaScript

We use a lot of JavaScript in this book. All the interaction and canvas graphics are done via script. Here are several resources that worth reading.

  • JavaScript and jQuery 101 by jQuery.

    The jQuery learning center contains detail guides to learn the concept of JavaScript and jQuery. It’s a good place to reference different object types. It also teaches you the jQuery concept and essential code.

  • Eloquent JavaScript by Marijn Haverbeke.

    This is a must have book if you want to learn JavaScript inside out. It covers the JavaScript programming for both client-side and browser-side.

  • JavaScript book by Jon Duckett.

    Jon Duckett has the ability to present programming skills in an easy-to-follow way. This JavaScript book demonstrates how JavaScript and jQuery works in beautiful and useful illustrations and charts.

  • JavaScript Best Practices by Thinkful.

    JavaScript can be evil if writing it badly. JavaScript Best Practices show different tricks to write maintainable and less buggy code.

  • JavaScript: The Right Way by William Oliveira.

    This is a big list of JavaScript resources containing latest practices and modern libraries.

  • JavaScript: The Good Part by Douglas Crockford.

    This book shows the beauty part of JavaScript. It presents the readers how JavaScript is different than the traditional object-oriented programming languages. JavaScript has its own way to write OOP.

CoffeeScript

CoffeeScript is beauty way to write JavaScript. It provides syntax that makes writing JavaScript application much easier. We will use CoffeeScript in some examples. Please note that CoffeeScript is JavaScript. It is like shorthand of JavaScript. When we write our program, we still think in JavaScript way. So you still need to learn JavaScript well.

  • Official site of CoffeeScript

    The official site contains all the syntax that we need to get started.

  • JS2Coffee converter

    At the beginning of learning CoffeeScript, you may find yourself struggling between JS and Coffee. This converter is a handy tool to convert the given script between JavaScript and CoffeeScript.

  • The little book of CoffeeScript

    If you prefer reading book than website, you’ll find this book useful as the reference guide to the CoffeeScript syntax.

  • CoffeeScript Cookbook

    This cookbook contains lots of practical examples for your daily JS development needs.

  • CoffeeScript Ristretto

    This book provides a journey of JavaScript and CoffeeScript development process. The examples in this book demonstrates how we can write code to get the job done at the first step, then fine tuning the code to make it more readable and maintainable.

Why CreateJS?

There are so many HTML5 game libraries out there. Why I choose CreateJS?

It is not a game engine. It is a library that takes graphics and animation drawing its main priority. Just like every one used Flash to make game, but still other creatives used Flash to build interactive applications. This is the same while using the CreateJS.

As a bonus, CreateJS integrates deeply in Adobe Flash. This allows us to create and export timeline-based animation to CreateJS’ canvas drawing JavaScript directly. That’s a very handy workflow if you use it probably.

Quick demo – advertisement animation

Here is a quick demo to show how Adobe Flash exports an advertisement animation and plays in HTML5 with CreateJS. The following example is the classic tween animation directly exported from Flash. I didn’t edit the output code.

Classic tween transition

Classic tween transition

How we make use of the generated code in our CreateJS project? We can instaniate the exported object add add it to the display list. The object name follows the name we set in the Flash IDE, either the linked class name or the library name. Then we can just add it to the CreateJS hierarchy by as using a normal Sprite object.

After we include the exported JavaScript file into our project, we can add the movieclip from the .fla library to the canvas display list by using the following code snippet. Here we assumed the clip name in Flash library is called ExampleSprite.

1 var sprite = new ExampleSprite();
2 this.stage.addChild(sprite);
Canvas output of Flash ad

Canvas output of Flash ad

This example code adds the exported animation on the stage for demonstration purpose. In your project, you may want to add the animation to the display list hierarchy.

I have created a video demonstrating the process to make this advertisement and export the animation to CreateJS. You can find the video in the following link.

http://mak.la/ad-demo

Example projects

I like to learn via practical projects. That’s why I make this book full of example projects that you can follow and modify them to fit into your real world projects.

We will build the following projects.

  • Project 1: Basic app with animated transitions

    In this project, we build a very basic canvas based app. The app demonstrates how we can create animated transition.

  • Project 1B: DOM-based app with animated transitions

    In this project, we modify the project 1 to make all the content an HTML DOM element. This allows the content to be accessible and yet we keep the animation in Canvas.

  • Project 2: Rain or Not?

    In this project, we separate the code into MVC model. That is dividing the code into Modal (Data), View and Controller modules. We also listen to the gyroscope in the mobile device to create a subtle motion parallax offset effect.

  • Project 3: Our solar system
  • Project 4: Drawing charts

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 instaler package.

Time for Action

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/app.coffee
     app/scripts/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 gulp coffee-script gulp-coffee gulp-concat gulp-order --save-dev
    
  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 \
     5 CreateJS",
     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      "gulp-order": "^1.1.1"
    18    }
    19  }
    

  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  order   = require 'gulp-order'
     5 
     6  gulp.task 'js', ->
     7    gulp.src [
     8      './app/scripts/setting.coffee'            
     9      './app/scripts/app.coffee'
    10    ]
    11    .pipe coffee()
    12    .pipe concat 'app.js'
    13    .pipe gulp.dest './app/scripts/'
    14 
    15 
    16  gulp.task 'watch', ->
    17    gulp.watch './app/scripts/**/*.coffee', ['js']
    18 
    19  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 and we should see an app.js file is generated with the following content.

    The generated 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.

You may need to run the local installed gulp which we can execute from the ./node_modules/.bin/gulp.

What’s happening?

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.

Further enhancement

In this example, we used GulpJS to compile our sources. If you prefer GUI setup, you may take a look at the LiveReload and CodeKit project.

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 repostory and host the files ourselves.

https://github.com/createjs

In our project, we’ll use the CDN version. But if you want to enable offline access of your web app, you may want to host your own CreateJS files to propally cache them.

Time for Action

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 
    13  /* canvases sit inside the #app frame. It’s similar to layers. */
    14  #app > canvas {
    15    position: absolute;
    16    top: 50%;
    17    left: 50%;
    18    height: 400px;
    19    width: 300px;
    20    margin-top: -200px;
    21    margin-left: -150px;
    22  }
    

  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  # alias
     5  cjs = createjs
     6  setting = this.exampleApp.setting
     7 
     8 
     9  class App
    10    # Entry point.
    11    constructor: ->
    12      console.log "Welcome to my portfolio."
    13      @canvas = document.getElementById("app-canvas")
    14      @stage = new cjs.Stage(@canvas)
    15 
    16      cjs.Ticker.setFPS 60
    17      cjs.Ticker.addEventListener "tick", @stage # make sure the stage refresh dr\
    18 awing for every frame.
    19 
    20 
    21  # Start the app
    22  new App()
    

  5. We have created the app’s foundation. Although we don’t see any content yet, the app structure is ready and we can add our scene to the app in next step.

What’s happening?

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.

If you want to provide an app like experience where users cannot zoom the view, you may consider adding the minimum-scale and maximum-scale value to the viewport. In contrasts, adding these two constants to web site will harm the user experience because website readers expect they can pinch to zoom any web pages.

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 homescreen, 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 dimention. 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 our code generates.

 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 by new 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.

    app.coffee


    1  Scene = this.exampleApp.Scene
    2 
    3  class App      
    4    constructor: ->
    5      ...
    6      # Temporary testing scene
    7      testScene = new Scene('gold')
    8      @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’s happening?

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  

If you need to draw complex shapes that are difficult to express in code, you may consider drawing the shapes in Adobe Flash and export it to JavaScript to use.

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.

Shape.js, from CreateJS


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

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’s happening?

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 elemnets 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’s happening?

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>

If you have modified the animation in Flash, you need to publish the Flash document again to update the JavaScript file.

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.

    class SceneA extends Scene constructor: ->

     1  ...
     2  info.on 'click', ->
     3    scene = new SceneInfo()
     4    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationA'
     5 
     6  ...
     7  photoA.on 'click', ->
     8    scene = new SceneB('a')
     9    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    10 
    11  ...
    12  photoB.on 'click', ->
    13    scene = new SceneB('b')
    14    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    15 
    16  ...
    17  photoC.on 'click', ->
    18    scene = new SceneB('c')
    19    sceneManager.pushSceneWithTransition scene, 'TransitionAnimationB'
    

What’s happening?

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       
     9    # get the original canvas dimension
    10    height = canvas.getAttribute('height')
    11    width = canvas.getAttribute('width')
    12       
    13    # set the new dimension with ratio multiplication
    14    canvas.setAttribute('width', Math.round(width * ratio))
    15    canvas.setAttribute('height', Math.round( height * ratio))
    16       
    17    # ensure the canvas CSS style follows the original dimension
    18    canvas.style.width = width+"px"
    19    canvas.style.height = height+"px"
    20       
    21    # scale the entire stage so we can use the original coordinate in our app.
    22    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’s happening?

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.

It’s worth noting that the retina display not only applies to mobile device but also desktops, such as Mackbook Pro Retina and the 5K retina iMac.

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.

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’s happening?

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.

    pushSceneWithTransition: (scene, transitionClassName=’TransitionAnimationB’) -> transition = new libtransitionClassName

     1    # The demension follows the Flash canvas dimension
     2    transition.x = 300/2
     3    transition.y = 400/2    
     4       
     5    $('#app-canvas').show()
     6       
     7    transition.on 'sceneShouldChange', =>
     8      @pushScene scene
     9         
    10    transition.on 'transitionEnded', ->
    11      $('#app-canvas').hide()
    12 
    13    @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’s happening?

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 scoll 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=''>

Project 2 – Rain or Not?

In this project, we create an app that helps the user knows if they need to begin the umbrella each day. It serves one single purpose which fetches the weather. In this example, we only care about the rainy day and the sunny say. In the future, you may modify it to support more weather conditions such as windy and snow days.

Before we get started into the project, here is the screenshot of whatwe are going to build. You can also try the project demo with the following link. I recommend you go try it so you can map the code with the app that we are building.

Screenshot of the app example

Screenshot of the app example

http://mak.la/demo-rain-or-not

Why this project is awesome

We have created an information based app in last example. In this example, we will explore how we can separate the logic into data, view and controlling logic. This is known as MVC, modal-view-controller.

We try to separate the logic, data and the view. This ensues that each module of code is minimal and so they are easy to maintain.

Mission Checklist

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

  1. Setup the project
  2. Data
  3. Mocking API
  4. View
  5. Adding Canvas
  6. Moving the Canvas in
  7. Aligning with FlexBox
  8. Device Rotation

The core logic of the app


1 ;(function($){
2 $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=Macao,MO&callback=?'\
3 , function(data){
4     console.log(data);
5   });
6 }).call(this, jQuery);

1. Setup the project

Time for Action

  1. The index.html file.

    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>Rain or Not</title>
     8      <link rel='stylesheet' href='styles/app.css'>
     9    </head>
    10    <body>
    11      <div id='app'>      
    12        <div id='main' class='page loading'>
    13          <header>Macao</header>
    14          <canvas id='app-canvas' class='out' width='300' height='300'>
    15            <!-- fallback content -->
    16            <img class='rainy-only status' src='http://placehold.it/300x300&text=\
    17 rainy' alt='rainy'>
    18            <img class='sunny-only status' src='http://placehold.it/300x300&text=\
    19 sunny' alt='sunny'>
    20          </canvas>  
    21          <p class='description rainy-only'>Bring your umbrella</p>
    22          <p class='description sunny-only'>Have a nice day!</p>
    23        </div>
    24      </div>
    25 
    26      <script src='//code.jquery.com/jquery.min.js'></script>
    27      <script src='//code.createjs.com/easeljs-0.7.1.min.js'></script>
    28      <script src='//code.createjs.com/tweenjs-0.5.1.min.js'></script>
    29      <script src='//code.createjs.com/movieclip-0.7.1.min.js'></script>
    30      <script src='scripts/rain-or-not-lib.js'></script>
    31      <script src='scripts/app.js'></script>
    32      <script src='//cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.m\
    33 in.js'></script>
    34    </body>
    35  </html>
    

  2. The app.coffee file.

    app.coffee


     1  this.rainOrNot = {}
     2 
     3  class App
     4    constructor: ->
     5      console.log "Do you need your umbrella today?"
     6       
     7      @refresh()  
     8 
     9 
    10    refresh: ->
    11      data = new Data()
    12      view = new View()
    13      data.fetch (is_rainy) -> 
    14        view.update(is_rainy)
    

  3. Make sure we invoke the App:

    app.coffee


    1  new App()
    

What just happened?

2. Data module

The data logic is responsible to fetch the data from the source and parse the data.

Time for Action

  1. We create a new class for the Data module.

    app.coffee


     1  class Data
     2    constructor: ->
     3      @api = 'http://api.openweathermap.org/data/2.5/weather?q=Macao,MO'
     4    fetch: (callback) ->
     5 
     6      $.getJSON @api, (data) ->
     7        console.log(data)
     8 
     9        code = data.weather[0].id + "" # force to string
    10 
    11        # rainy code all start at 5
    12        if code[0] == '5'
    13          callback(true)
    14        else
    15          callback(false)
    

3. Mocking API

We need to use different API response to test our logic.

Mocking an API usually means that we create a static JSON file and put it somewhere that the development app can access. But this could be more automatic if we have lots of mock API to create. A tool named mockable comes to help.

I created 2 canned responses, sunny and rainy, with the following URL. The response JSON is copied from the source, OpenWeather.

1 http://demo5385708.mockable.io/weather?sunny
2 http://demo5385708.mockable.io/weather?rainy

Time for Action

  1. In the Data class, we add a mock API to test different API response. Add the following code to override the API url.

    app.coffee


    1  class Data
    2    constructor: ->
    3      @api = 'http://api.openweathermap.org/data/2.5/weather?q=Macao,MO'
    4 
    5      # mock
    6      @api = 'http://demo5385708.mockable.io/weather?rainy'
    7    ...
    

4. View

Defining the CSS styles.

 1 .loading {
 2   background-image: url(../images/loading.png) center center no-repeat;
 3 }
 4 
 5 .sunny {
 6   background: #B8DCF1
 7 }
 8 .rainy {
 9   background: #9FB6C4;
10 }

The main view that controls DOM elements and the more-specific View objects, such as Background and CanvasView

 1 class View
 2   constructor: ->
 3     $('.status').hide()
 4     $('.description').hide()
 5 
 6     @canvasView = new CanvasView()
 7     @canvasView.reset()
 8     @background = new Background()
 9   update: (is_rainy=true)->
10     $('.loading').removeClass('loading')
11     @canvasView.moveIn()
12     if is_rainy
13       $('.rainy-only').show()
14       $('.sunny-only').hide()
15       @canvasView.showRainy()
16       @background.setRainyBackground()
17     else
18       $('.rainy-only').hide()
19       $('.sunny-only').show()
20       @canvasView.showSunny()
21       @background.setSunnyBackground()

Controlling the Background

1 class Background
2   constructor: ->
3     @element = $('body')
4   setSunnyBackground: -> @element.addClass('sunny')
5   setRainyBackground: -> @element.addClass('rainy')

5. CanvasView

 1 class CanvasView
 2   constructor: ->
 3     cjs = createjs
 4     @canvas = document.getElementById("app-canvas")
 5     @stage = new cjs.Stage(@canvas)
 6 
 7     cjs.Ticker.setFPS 60
 8     cjs.Ticker.addEventListener "tick", @stage
 9     cjs.Ticker.addEventListener "tick", @tick
10 
11     @retinalize()
12   tick: =>
13     @applyDeviceRotation()
14 
15   retinalize: ->
16     CanvasView.width ?= @canvas.width
17     CanvasView.height ?= @canvas.height
18 
19     @canvas.style.width = CanvasView.width + 'px'
20     @canvas.style.height = CanvasView.height + 'px'
21     @canvas.width = CanvasView.width * 2
22     @canvas.height = CanvasView.height * 2
23     @stage.scaleX = @stage.scaleY = 2
24 
25   moveIn: -> $(@canvas).removeClass('out').addClass('in')
26   reset: -> $(@canvas).removeClass().addClass('out')
27 
28   showRainy: ->
29     @icon = new lib.Rainy()
30     @stage.addChild @icon
31   showSunny: ->
32     @icon = new lib.Sunny()
33     @stage.addChild @icon

6. Moving the canvas in

Time for Action

This is a subtle effect that we move the canvas icon from bottom to the center of the viewport by using CSS3 transition.

app.css file

 1 .out {
 2   transform: translateY(100%);
 3   opacity: 0;
 4 }
 5 
 6 .in {
 7   transition: all .75s cubic-bezier(0.140, 0.460, 0.160, 1.210);
 8   transform: translateY(0%);
 9   opacity: 1;
10 }

The transition may fail if the data is cached without loading. We need to add a little delay to make the CSS transition work.

1 class App
2   constructor: ->
3     console.log "Do you need your umbrella today?"
4 
5     setTimeout =>
6       @refresh()
7     , 500

7. Aligning with FlexBox

Flexbox is the next hot topic on layout. It’s draft was first published in 2009 and has reach a relatively stable status now, after 5 years of discussions and name changes.

In the step, we will make the app frame a flexbox container and layout our user interface elements at the center of the screen, vertically.

Time for action

  1. Add the following to the CSS to make the items align center vertically.

    app.css


     1  /* Flexbox */
     2  html, body, #app, .page {
     3    height: 100%;
     4  }
     5  .page {
     6    flex-direction: column;
     7    display: flex;
     8    align-items: center;
     9    justify-content: center;
    10  }
    

I recommend reading the full guide to FlexBox. Or you may check the latest working draft on W3C website.

http://www.w3.org/TR/css-flexbox-1/

8. Device Rotation

In this step, we will add a motion effect where the rainy and sunny icon slightly moves based on the device rotation.

This is done by listening to the DeviceOrientationEvent and use the rotation degree to control the icon.

Time for Action

Let’s work on the following steps to create a motion effects based on the rotation degree of the device.

  1. We create a new class which keep storing the latest device rotation. The value is from the Gyroscope where axises are represented in alpha, beta, gamma.

    app.coffee


     1  class DeviceRotation
     2    constructor: ->
     3      DeviceRotation.a = DeviceRotation.b = DeviceRotation.g = 0
     4 
     5      # gyroscope
     6      $(window).on 'deviceorientation', (e)->
     7        DeviceRotation.a = @a = e.originalEvent.alpha
     8        DeviceRotation.b = @b = e.originalEvent.beta
     9        DeviceRotation.g = @g = e.originalEvent.gamma
    10        $('#debug').text("#{@a} #{@b} #{@g}")
    

  2. We need to apply the value. In the CanvasView class, we use the device rotation to offset the sunny and rainy icons.

    app.coffee


     1  applyDeviceRotation: ->
     2    a = DeviceRotation.a
     3    b = DeviceRotation.b
     4    g = DeviceRotation.g
     5 
     6    @icon.front.x = CanvasView.width/2 + g/10
     7    @icon.front.y = CanvasView.height/2 + b/10
     8 
     9    @icon.back.x = CanvasView.width/2 + g/5
    10    @icon.back.y = CanvasView.height/2 + b/5
    

  3. Now the App constructor becomes:

    app.coffee


     1  class App
     2    constructor: ->
     3      console.log "Do you need your umbrella today?"
     4       
     5      setTimeout =>
     6        @refresh()
     7      , 500
     8 
     9      $('body').click => @refresh()
    10 
    11      new DeviceRotation()
    

What happened?

The #debug element is used to observe the value of the gyroscope. By printing out the values, we can hold the device on hand, and then rotate the device into different directions and observe how the tilting changes the three rotation axises. After we get what we need from the numbers, we can hide it or even delete it. Make sure these debug information is not visible when we deploy the app in production environment.

Device Orientation Event

For more information on using the 3 axises of the device rotation, please check the Apple Developer documentation:

http://mak.la/apple-device-orientation-event

I created a utility that inspects the device rotation value and prints them out nicely.

https://play.google.com/store/apps/details?id=net.makzan.gyroinspcetor

You may use this tool to inspect the value you want by holding the device at the target rotation.

Controlling Flash instance

When we set an instance name in the Flash movie clip which being exported to canvas, we can actually access that instance from the Javascript. This gives a huge convenient way to manipulated with the exported graphics.

For example, in this example, we listen to the drive rotating events and changes the movement offset of both the front and back instance of the weather symbol. The front and back instance are already defined in the flash and exported to the Javascript.

Summary

In this project, we learnt to define our logic into different modules for easier maintenance. The logic is divided into data querying, view rendering and controller that bridges between data and view.

We also learned to control pre-defined symbol instances that was exported from Adobe Flash.

At last, we listen to the device orientation to get the 3 axis value of device rotation.

Project 3 – Solar System

This chapter is not yet completed and it requires further re-written.

In this project, we are going to create a tool that presents our solar system.

The system is constructed by HTML DOM elements and CSS transform and transition.

Why is this project awesome?

By following the project steps, you will learn how to scroll the view in a parallax effect. This effect created an illusion of depth. That’s based on our real world that’s in perspective view. Things aren’t look the same at different distances. Things that’s far away from us looks smaller and moves slower to us. By creating the same scaling and movement at different layers, we can create similar depth feeling.

Preparation

Before we get started, we want to include one more Gulp plugin.

If you are using an existing project example, you may run the following command to add the gulp-sass compiler to the project.

1 npm install gulp-sass --save-dev

If you are working on a new folder, you may download the following skeleton which contains the basic project file structure and the package.json file.

1 http://mak.la/cjs-skeleton

Make sure to run npm install --save-dev after you download the skeleton code.

SCSS is a kind of CSS preprocessor. Preprocessor means that we write another syntax to express our css rules and then compile it into css. The beauty of scss is that it is like css. If you don’t use any of the scss specific syntax, the code is actually valid css file.

HTML

This is the HTML skeleton.

 1 <div id='app'> 
 2   <div id='solar-system' class='focus-earth'>
 3     <div class='layer deep-bg'></div>
 4     <div class='layer bg'></div>
 5     <div class='layer planets'>
 6       <!-- each planet here -->
 7     </div>
 8   </div>
 9   <div class='info-panel'>
10     <!-- Info text here -->
11   </div>
12   <div class='buttons'>      
13     <!-- button to focus on for every planet -->
14   </div>
15   
16   <div class='detail-panel out'>
17     <!-- pop up panel with detail information on every planet -->
18   </div>
19 </div>

Almost the elements are position absolute. The #app element is relative position to act as the relative coordinate for all the absolute position child elements. They all overlap together. Then we adjust the transform and too left bottom right to control their position.

We use absolute position when we want the layers (groups) to overlap together.

The solar system had three layers. The deep background, background and the planets. The two background layers is essential to create a space movement illusion.

In the futuron, there may be additional layers such as the sun and a front layer of dust.

Basic CSS:

 1 * {
 2   box-sizing: border-box;
 3 }
 4 
 5 body, div, ul, li, h1, h2, h3, p {
 6   margin: 0;
 7   padding: 0;
 8 }
 9 
10 img {
11   max-width: 100%;
12 }
13 
14 html, body, #app, #solar-system {
15   width: 100%;
16   height: 100%;
17 }
18 
19 
20 #app {
21   position: relative;  
22   overflow: hidden;
23 }

Planet buttons

HTML:

 1 <div class='buttons'>
 2     <a href='#mercury' class='focus-button'>Mercury</a>
 3     <a href='#venus' class='focus-button'>Venus</a>
 4     <a href='#earth' class='focus-button'>Earth</a>
 5     <a href='#mars' class='focus-button'>Mars</a>
 6     <a href='#jupiter' class='focus-button'>Jupiter</a>
 7     <a href='#saturn' class='focus-button'>Saturn</a>
 8     <a href='#uranus' class='focus-button'>Uranus</a>
 9     <a href='#neptune' class='focus-button'>Neptune</a>
10   </div>

CSS:

 1 /* User Inteface */
 2 .buttons {
 3   position: absolute;
 4   height: 100%;
 5   right: 0;
 6   
 7   display: flex;
 8   flex-direction: column-reverse;
 9   
10   a {
11     display: block;
12     flex: 1;
13     
14     text-decoration: none;
15     color: #555;
16     
17     line-height: 50px;
18     
19     &.active {
20       color: white;
21     }
22   }
23 }

The button is laid out using the flexbox to ensure they are evenly distributed.

The nested scope in the preprocessor allows us to group related styles together. We can modularize the styles to specific part of the app.

The solar system

 1 $space-height: 13000px;
 2 $bg-z: 10;
 3 $deep-bg-z: 30;
 4 
 5 #solar-system {    
 6   position: absolute;
 7   overflow: hidden;  
 8   
 9   transform-origin: 0 0;
10 }
11 
12 
13 .layer {
14   position: absolute;
15   transition: all 1.5s ease-out;
16 }
17 
18 .deep-bg {
19   width: 100%;
20   height: $space-height;
21   background: black url(../images/deep-bg.jpg);
22 }
23 
24 .bg {
25   width: 100%;
26   height: $space-height;
27   background: url(../images/bg.jpg);
28   opacity: .3;
29 }
30 
31 .planet {
32   position: absolute;   
33   
34   img {
35     max-width: 80px;  
36   }
37 }

Those preprocessors usually allow us to define variable and expressions. For example, we defined several variables for our solar system. They are the height of the space view. The virtual Z index of the layers.

We don’t need the height of the space view if we order the planets from top to bottom.

We need the height because we are calculating the position from the bottom.

Showing the planets

The value of the planet is based on the average distance the real planet are away from the sun. We have a multiplier to adjust the scale.

 1 $planets: "mercury" "venus" "earth" "mars" "jupiter" "saturn" "uranus" "neptune";
 2 $distances:  57*2px 108*2px 150*2px 228*2px 779*2px 1430*2px 2880*2px 4500*2px;  
 3 $x-positions: 30vw 15vw 45vw 50vw 35vw 60vw 25vw 56vw;
 4 
 5 @for $i from 1 through length($planets) {
 6   $name: nth($planets, $i);
 7   $distance: nth($distances, $i);
 8   $x-position: nth($x-positions, $i);
 9   .#{$name} {
10     transform: translateX($x-position) translateY(calc(#{$space-height} - #{$dis\
11 tance}));  
12   }
13 
14   ...
15 }

The looping expressions allows us to define similar css rules without explicitly typing all of them. For example, we use loop and a list to iterate all 8 planets rules and their styles when focused.

Scss list is useful when we cant express the calculation with just the looping index. For example, we want to iterate the name of the 8 planets, so we use a list to store the planet name and use it to define the class based on these names.

You may think that list is kind of array in programing language. Actually scss even has a nested list like a 2 dimension array.

There are some utility functions to help us manipulate the scss list. The most common one is the nth function where we get the value based on the index, usually within an for loop. The length function helps us to define the ending condition if the for loop.

The calc function in css allows is to express a calculation as a property value.

Please note that css does support using expression as property value, it is the calc function. And css also supports variable. But variable are supported in Firefox only right now. So we have to mix the scss variable and the css calc. By mixing them, we need to use the #{} syntax to distinguish the scss expression and the css expression.

 1 @for $i from 1 through length($planets) {
 2   $name: nth($planets, $i);
 3   $distance: nth($distances, $i);
 4 
 5   ...
 6 
 7   #solar-system.focus-#{$name} {
 8     .planets.layer {
 9       transform: translateY(calc(-#{$space-height} + #{$distance} + 40vh));    
10     }
11     .bg.layer{
12       transform: translateY(calc((-#{$space-height} + #{$distance} + 40vh) / #{$\
13 bg-z}));
14     }
15     .deep-bg.layer{
16       transform: translateY(calc((-#{$space-height} + #{$distance} + 40vh) / #{$\
17 deep-bg-z}));
18     }
19   }
20 }

The focus style explicitly moves the 3 layers into different transform Y based on the calculation.

By toggling different focus styles on different planets, the 3 layers move in different speeds and thus created the parallax effect.

Detail Panel

1 <div class='detail-panel out'>
2   <div id='tab-mercury' class='tab'>          
3     <h1>Mercury</h1>
4     <img src='images/mercury.jpg' alt='Mercury'>
5     <p><small>Photo by NASA. Public Domain.</small></p>
6     <p><small>Tap anywhere to close</small></p>
7   </div>
8   <!-- other planets detail go here -->
9 </div>

Project 4 – Countries Area

In this project, we create an app that draws a chart by using canvas and CreateJS.

This example demonstrated both multiple select list and single item select list.

The multiple select list is done via check box with array as name. The single item selection list is done by using the radio buttons.

The beauty of using these two basic elements is that it works perfect without any css and Javascript. The css and Javascript is here to enhance the select list. But the core thing still works without these enhancement.

Why is this project awesome?

This project you will learn tobmake momentum list. Learn to customize the radio and checkbox. Learn to draw basic chart using the create js library. Also learn to use flex box to create the entire app layout.

The project is divided into the following steps.

  1. Building the app layout with flex
  2. Listing the countries data
  3. Basic list selection and calculation
  4. Styling the radio and checkbox list
  5. Drawing the chart
  6. Adding the info panel and global app style

Preparing the project

Before we get started the project, let’s prepare several files. They are:

  1. Gulpfile.coffee
  2. retinalize.coffee
  3. shape.coffee

Gulpfile.coffee


 1 gulp.task 'js', ->
 2   gulp.src [
 3     './app/scripts/data.coffee'
 4     './app/scripts/retinalize.coffee'
 5     './app/scripts/shape.coffee'
 6     './app/scripts/chart.coffee'
 7     './app/scripts/view.coffee'
 8     './app/scripts/app.coffee'
 9   ]
10   .pipe coffee()
11   .pipe concat 'app.js'
12   .pipe gulp.dest './app/scripts/'

We make the retinalize class is ready.

 1 this.utility ?= {}
 2 
 3 this.utility.retinalize = (stage, updateCSS=true) ->
 4   canvas = stage.canvas
 5   utility.originalCanvasWidth = canvas.width
 6   utility.originalCanvasHeight = canvas.height
 7   
 8   return unless window.devicePixelRatio
 9 
10   ratio = window.devicePixelRatio      
11 
12   height = canvas.getAttribute('height')
13   width = canvas.getAttribute('width')
14 
15   canvas.setAttribute 'width', Math.round( width * ratio )
16   canvas.setAttribute 'height', Math.round( height * ratio )
17 
18   if updateCSS
19     canvas.style.width = width+"px"
20     canvas.style.height = height+"px"
21   
22   
23   stage.scaleX = stage.scaleY = ratio

Then we have another utility class that draws rectangle shape.

shape.coffee


 1 # alias
 2 cjs = createjs
 3 
 4 class this.DefaultShape extends cjs.Shape
 5   constructor: (@options={}) ->
 6     super()
 7     @initialize()
 8     @applyOptions()
 9   applyOptions: ->
10     @options.fillColor ?= null
11     @options.strokeColor ?= null
12     @options.strokeWidth ?= 1
13     @options.width ?= 100
14     @options.height ?= 100
15     @options.x ?= 0
16     @options.y ?= 0
17     return @options
18   
19 # Shapes
20 # options:
21 # width, height, fillColor, strokeColor, strokeWidth
22 class this.RectShape extends this.DefaultShape
23   constructor: (options={}) ->
24     super(options)        
25     @graphics
26       .setStrokeStyle @options.strokeWidth
27       .beginFill @options.fillColor
28       .beginStroke @options.strokeColor
29       .drawRect 0, 0, @options.width, @options.height
30     @x = @options.x
31     @y = @options.y

We’ll need the RectShape when we draw the chart in canvas.

There is basic CSS reset in the app.css file.

app.css


1 /* Basic reset */
2 html, body, p, ul, li, h1, h2, h3, div {
3   margin: 0;
4   padding: 0;
5 }
6 
7 * {
8   box-sizing: border-box;
9 }

Step 1 – Building the app layout with flex

In this task, we build the basic layout by using the flex box. We will create our own minimal flex layout styles. Every elements with class .container will treat as flex display. All their children would have flex:1 1 auto by default, unless .shrink class presents.

Time for Actions

Let’s follow the steps to create the app layout by using flex box.

  1. In the #app in HTML, we add the following elements.

    index.html


     1  <div id='app' class='container vertical'>
     2    <div class='charts container'>
     3      <div><canvas width="300" height="150"></canvas></div>
     4      <div><canvas width="300" height="150"></canvas></div>
     5    </div>  
     6    <div class='container'>
     7      <div class='list container vertical'>  
     8        <p class='description'>Area: <span class='output1'>0</span>K km<sup>2</su\
     9 p></p>
    10        <ul id='countries-on-left'>
    11          <li>List Item</li>
    12          <!-- lots of list times -->
    13          <li>List Item</li>
    14        </ul>
    15      </div>
    16      <div class='list container vertical'>      
    17        <p class='description'>Area: <span class='output2'>0</span>K km<sup>2</su\
    18 p></p>
    19        <ul id='countries-on-right'>
    20          <li>List Item</li>
    21          <!-- lots of list times -->
    22          <li>List Item</li>
    23        </ul>
    24      </div>  
    25    </div>
    26  </div>
    

  2. The minimal flex-based layout.

    app.css


     1  /* Minimal flex grid */
     2  .container {
     3    display: flex;
     4  }
     5  .container.vertical {
     6    flex-direction: column;
     7  }
     8  .container > * {
     9    flex: 1 1 auto;
    10    border: 1px solid green; /* debug */
    11  }
    12  .container .shrink {
    13    flex: 0 1 auto;
    14  }
    

  3. For the flex to work perfectly, we give a width and height to the container, which is the HTML and body element in this case.

    app.css


     1  /* Global */
     2 
     3  html, body {  
     4    width: 100%;
     5    height: 100%;
     6  }
     7  #app {
     8    width: 100%;
     9    height: 100%;
    10    background: IVORY;
    11  }
    

  4. The flex layout will change the dimension on the children elements. We can specific a minimal width and height so the flex layout will keep a minimal space for the elements.

    app.css


     1  /* Canvas */
     2  canvas {
     3    max-width: 100%;
     4    min-height: 150px;
     5  }
     6 
     7  .charts {
     8    min-height: 150px;
     9  }
    10     
    11  /* Area Description */
    12  p.description {
    13    min-height: 30px;
    14  }
    

  5. Finally, we make the long list overflow:scroll and enable the momentum scrolling.

    app.css


    1  /* List */
    2  ul {
    3    overflow-x: hidden;
    4    overflow-y: scroll;
    5    -webkit-overflow-scrolling: touch;
    6    list-style: none;
    7    padding: 5px;
    8  }
    

We created a minimal flex based layout.

For every .container class, we display the children as flex items.

The children inside the container has flex: 1 1 auto by default.

 1 .container                                                                    
 2 +----------------------------------------------------+                        
 3 | .container               .container                |                        
 4 | +----------------------+ +-----------------------+ |                        
 5 | |                      | |                       | |                        
 6 | |                      | |                       | |                        
 7 | |                      | |                       | |                        
 8 | |                      | |                       | |                        
 9 | |                      | |                       | |                        
10 | +----------------------+ +-----------------------+ |                        
11 |                                                    |                        
12 +----------------------------------------------------+                        
13 |                                                    |                        
14 |                                                    |                        
15 |                                                    |        .container {    
16 |                                                    |          display: flex;
17 |                                                    |        }               
18 |                                                    |                        
19 |                                                    |                        
20 |                                                    |                        
21 |                                                    |                        
22 |                                                    |                        
23 |                                                    |                        
24 |                                                    |                        
25 |                                                    |                        
26 |                                                    |                        
27 |                                                    |                        
28 |                                                    |                        
29 |                                                    |                        
30 |                                                    |                        
31 +----------------------------------------------------+                        

When the content exceeds the DOM container, we can use overflow scroll to make the content scroll inside the container. But this scroll wont have the momentum scrolling which common in the touch device. We need to add he the webkit scrolling to enable the momentum scrolling.

Step 2 – Listing the countries data

In this task, we obtain the list of countries areas and render them into the 2 lists we create in last step.

  1. In last task, we created a long list to test the layout. Now we don’t need that list anymore because we are rendering the list dynamically. Replace the 2 ul elements into the following.

    index.html


    1  <ul id='countries-on-left'>
    2    <li class='template'><label><input type='radio' name='target-country'><span c\
    3 lass='name'>China</span></label></li>
    4  </ul>
    5  ...
    6  <ul id='countries-on-right'>
    7    <li class='template'><label><input type='checkbox' name='countries[]'><span c\
    8 lass='name'>China</span></label></li>
    9  </ul>
    

  2. The country data is obtained from Wikipedia. We put them into an array of object. For each object, we store the country name and the area.

    data.coffee


     1  this.app ?= {}
     2  this.app.data ?= {}
     3 
     4  # Area of common countries
     5  # Source from http://simple.wikipedia.org/wiki/List_of_countries_by_area
     6  this.app.data.areaOfCountries = [
     7    {name: 'China', area: 9651.747}
     8    {name: 'Russia', area: 17098.242}
     9    {name: 'Canada', area: 9889.000}
    10    {name: 'USA', area: 9826.675}
    11    {name: 'Australia', area:9596.691}
    12    ...
    

  3. We have the data now, the next step is to render the data into the HTML list.

    view.coffee


     1  this.app ?= {}
     2 
     3  this.app.renderList = ->
     4    # List
     5    template = $('#countries-on-left').find('.template')
     6    countriesOnLeft = $('#countries-on-left')
     7    for country in app.data.areaOfCountries  
     8      clone = template.clone().removeClass('template')
     9      clone.find('input[type="radio"]').val(country.area)
    10      clone.find('.name').text(country.name)
    11      countriesOnLeft.append clone
    12    # Remove template after cloning done.
    13    template.remove()
    14 
    15    template = $('#countries-on-right').find('.template')
    16    countriesOnLeft = $('#countries-on-right')
    17    for country in app.data.areaOfCountries  
    18      clone = template.clone().removeClass('template')
    19      clone.find('input[type="checkbox"]').val(country.area)
    20      clone.find('.name').text(country.name)
    21      countriesOnLeft.append clone
    22    # Remove template after cloning done.
    23    template.remove()
    

  4. At last, we render the list in the app.coffee.

    app.coffee


    1  this.app ?= {}
    2 
    3  this.app.renderList()
    

What just happened?

Some JavaScript tutorial may show you to render HTML directly inside the JavaScript.

I prefer using the template approach where the template element of the radio list item and checkbox list item are defined inside the HTML. When I need it, I clone the template and update the data inside.

In this example, we use the template at the initial stage and we don’t need it after the setup logic, so we can remove the clone after the list creation. In some projects, we may need the template at unknown time after the project is setup. In this case, we can hide all the template elements by using .template{display:none}.

Step 3 – Basic list selection and calculation

In this task, we handle the radio and checkboxes clicking and calculate the sum of area of selected countries.

Time for Action

Let’s follow the steps to handle the radio and checkbox selection.

  1. In the view.coffee, we add a function that check the input changes and display the calculation.

    view.coffee


     1  this.app.handleListChange = ->
     2    # Toggle Chart 1 
     3    $('input[type="radio"]').change ->
     4      value = $('input[type="radio"]:checked').val()
     5      $('.output1').text(Math.round(value))
     6 
     7 
     8    # Toggle Chart 2
     9    $('input[type="checkbox"]').change ->
    10      sum = 0
    11      $('input[type="checkbox"]:checked').each ->
    12        sum += $(this).val()*1
    13      $('.output2').text(Math.round(sum))
    

  2. In the app.coffee file, we register the input changes handling by calling the function by the end of the logic.

    app.coffee


    1  this.app.handleListChange()
    

What just happened?

We handled the checkbox and radio selection.

The logic is based on the pseudo class :checked to get the HTML element of the checked input.

Step 4 – Styling the radio and checkbox list

In this project, we customize the radio and checkbox styles.

Time for Action

  1. All the customization are done is CSS. Add the following style in the app.css file.

    app.css


     1  /* Styling Radio and Checkbox */
     2  input[type='radio'],
     3  input[type='checkbox']{
     4    display: none;    
     5  }
     6 
     7  input[type='radio'] + .name,
     8  input[type='checkbox'] + .name{
     9    display: block;
    10    font-size: 1rem;  
    11    padding: 1rem .5rem;
    12    padding-left: 2rem;
    13    border: 1px solid transparent;
    14    border-left: 0;
    15    border-right: 0;
    16    transition: all .3s ease-out;  
    17  }
    18 
    19  input[type='radio']:checked + .name,
    20  input[type='checkbox']:checked + .name{
    21    border-color:DARKORANGE ;    
    22  }
    23 
    24  /* Radio specific */
    25  input[type='radio'] + .name {
    26    background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio.svg)\
    27  10px 50% no-repeat; 
    28    background-size: 16px;
    29  }
    30  input[type='radio']:checked + .name {
    31    background: SNOW url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio\
    32 -checked.svg) 10px 50% no-repeat;
    33    background-size: 16px;
    34  }
    35 
    36  /* Checkbox specific */
    37  input[type='checkbox']:checked+.name {
    38    background: SNOW url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/check\
    39 ed.svg) 10px 50% no-repeat;
    40    background-size: 16px;
    41  }
    

What just happened?

We customize the radio and checkbooks by hiding the default browser rendered radio and checkbooks button. Then we use the label to customize the button graphic

The label works because clicking in the label is identical to clicking on the input. That means when we click on the label, we are toggling the real radio boxes and check boxes.

So we can define the :checked style inn thr css where we rely to customize our graphics

We used the svg format for the checkbox and radio box graphics. We could use png. The reason we use svg is because the vector format scale better on the retina display and look sharper than using png format.

Step 5 – Drawing the chart

In this task, we draw the chart by using canvas.

Time for Action

  1. We add the #chart1-canvas and #chart2-canvas id to the canvas, so that our JavaScript logic can reference them.

    index.html


    1  <div class='charts container'>
    2    <div><canvas id="chart1-canvas" width="300" height="150"></canvas></div>
    3    <div><canvas id="chart2-canvas" width="300" height="150"></canvas></div>
    4  </div> 
    

  2. The chart logic is encapsulated into the chart.coffee file. {title=”chart.coffee”} this.app ?= {}
     1  # alias
     2  cjs = createjs
     3 
     4  class this.app.Chart
     5    # Entry point.
     6    constructor: (canvasId)->
     7      @stage = new cjs.Stage(canvasId)
     8             
     9      utility.retinalize @stage, false
    10      @canvasWidth = utility.originalCanvasWidth
    11         
    12          
    13    drawChart: (value) ->    
    14      @stage.removeAllChildren()
    15      # each tile = 10K km2
    16      areaForEachTile = 100
    17      tileWidth = tileHeight = 10
    18      margin = 5
    19      numberOfTiles = value / areaForEachTile
    20      cols = Math.floor((@canvasWidth-margin) / (tileWidth + margin))
    21         
    22      for i in [0...numberOfTiles]      
    23        x = margin + Math.floor(i % cols) * (tileWidth + margin)
    24        y = margin + Math.floor(i / cols) * (tileHeight + margin)
    25        shape = new RectShape
    26          fillColor: 'ORANGERED'
    27          width: tileWidth
    28          height: tileHeight      
    29          x: x
    30          y: y
    31        @stage.addChild shape
    32           
    33      # Draw on canvas
    34      @stage.update()
    
  3. In the handleListChange function in view, we update the code to call the chart to draw the value.

    view.coffee


     1  this.app.handleListChange = (chart1, chart2)->
     2    # Toggle Chart 1 
     3    $('input[type="radio"]').change ->
     4      value = $('input[type="radio"]:checked').val()
     5      $('.output1').text(Math.round(value))
     6      chart1.drawChart(value)  
     7 
     8    # Toggle Chart 2
     9    $('input[type="checkbox"]').change ->
    10      sum = 0
    11      $('input[type="checkbox"]:checked').each ->
    12        sum += $(this).val()*1
    13      $('.output2').text(Math.round(sum))
    14      chart2.drawChart(sum)
    

  4. Finally, we create the chart instance and tell the view to draw the chart after any input changes.

    app.coffee


    1  chart1 = new app.Chart("chart1-canvas")
    2  chart2 = new app.Chart("chart2-canvas")
    3 
    4  this.app.handleListChange(chart1, chart2)
    

 1 <----------------+  canvas_width +------------------------------------>         
 2                                                                                 
 3 +--------------------------------------------------------------------+          
 4 |                                                                    |          
 5 |  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  |          
 6 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |          
 7 |  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  |          
 8 |       ^                                                            |          
 9 |       |             ^ tile_width                                   |          
10 |       |                                                            |          
11 |       +                                                            |          
12 |      margin                                                        |          
13 |                                                                    |          
14 |                                                                    |         
15 |                                                                    |          
16 |                                                                    |          
17 |                                                                    |          
18 |   tiles_per_row = (canvas_width - margin) / (tile_width + margin)  |          
19 |                                                                    |          
20 |                                                                    |          
21 |   x = index % tiles_per_row                                        |          
22 |                                                                    |          
23 |   y = Math.floor(index / tiles_per_row)                            |          
24 |                                                                    |          
25 |                                                                    |          
26 |                                                                    |          
27 |                                                                    |          
28 +--------------------------------------------------------------------+          

Step 6 – Adding the info panel and global app style

In the last step, we add an information page and fine tune the global styles.

The info panel is presented by CSS 3d that rotate in from the left screen.

Time for Action

  1. We add an #info-btn that trigger the info panel.

    index.html


    1  <div id="info-btn">
    2    <a href="#info"><span>Info</span></a>
    3  </div>
    

  2. The #info-panel contains basic content.

    index.html


     1  <div id='info-panel'>
     2    <h1>Countries Area</h1>
     3    <p>This tool let you compare the area of some countries.</p>
     4    <p>Select countries on both list and compare them. Each tile in the chart is \
     5 100K km<sup>2</sup></p>
     6    <p class='more-space'>Tap anywhere to begin.</p>
     7    <p><small>Note: We only list several countries in this demo. <br>The source i\
     8 s from <a href="http://simple.wikipedia.org/wiki/List_of_countries_by_area">wiki\
     9 pedia</a>.</small></p>
    10  </div>
    

  3. The info-btn sits on the top right corner.

    app.css


     1  /* Info Button */
     2  #info-btn {
     3    position: absolute;
     4    top: 0;
     5    right: 0;
     6  }
     7  #info-btn a{
     8    width: 44px;
     9    height: 44px;
    10    display: block;  
    11       
    12    background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/info.svg);\
    13       
    14  }
    15  #info-btn a span {   
    16    display: none;
    17  }
    

  4. The #info-panel is full screen that rotate into the view in 3D.

    app.css


     1  /* Info panel */
     2  .hidden {
     3    transform: rotateY(-90deg);
     4  }
     5  .show {
     6    transform: rotateY(0);
     7  }
     8  #info-panel {
     9    position: absolute;
    10    top: 0;
    11    left: 0;
    12    width: 100%;
    13    height: 100%;
    14    background: ORANGERED;
    15    color: white;
    16       
    17    transform-origin: 0 0;
    18    transition: all .3s ease-out;
    19       
    20    display: flex;
    21    flex-direction: column;
    22    justify-content: center;
    23    align-items: center;  
    24       
    25    text-align: center;
    26  }
    27  #info-panel a {
    28    color: white;  
    29  }
    30  #info-panel p {
    31    margin: .5em;    
    32  }
    33  p.more-space {
    34    margin: 2em;
    35  }
    

  5. For the rotate 3D effect, we add the perspective to body. We also fine tune the global style here.

    app.css


    1  body {
    2    perspective: 700px;
    3    font-family: Verdana, sans-serif;
    4    font-size: 12px;
    5 
    6    padding: 5px;
    7    background: ORANGERED;
    8  }
    

  6. Finally, make sure we have removed the debugging style.

    app.css


    1  .container > * {
    2    border: 1px solid green; /* debug */
    3  }
    

What just happened?

We have created a panel transition by using CSS 3D effects.

Summary

We learnt a lot in the chapter. We created a simple app that let user select countries and display their area in a tile bar chart. In conclusion, after reading this chapter, you should be able to:

  • Build app layout by using CSS flex box.
  • Draw basic chart by using canvas and CreateJS library.

Project 4B – Drawing charts on canvas

In this project, we modify previous chapter to draw different types of chart by using the canvas tag and CreateJS library. We will also animate the chart by using the TweenJS, which is part of the CreateJS suite.

Mission Checklist

  1. Animated bar chart
  2. Animated tiles
  3. Pie chart
  4. Animated pie chart

Preparation

In the chart.coffee, we have following alias declared for easier access to the CreateJS, TweenJS and the Ease library.

chart.coffee


1 # alias
2 cjs = createjs
3 Ease = cjs.Ease
4 Tween = cjs.Tween

Animated Bar Chart

There is not much difference in this project and previous project, in terms of app structure and logic flow. The main difference is in the chart.coffee where we draw different type of chart.

  1. We prepare the structure of Chart class.

    chart.coffee


     1  this.app ?= {}
     2 
     3  # alias
     4  cjs = createjs
     5  Ease = cjs.Ease
     6 
     7  class this.app.Chart
     8    # Entry point.
     9    constructor: (canvasId)->
    10      @stage = new cjs.Stage(canvasId)
    11      cjs.Ticker.setFPS(60);
    12      cjs.Ticker.addEventListener 'tick', @stage
    13         
    14      utility.retinalize @stage, false
    15      @canvasWidth = utility.originalCanvasWidth
    16      @canvasHeight = utility.originalCanvasHeight    
    17         
    18      @initChart()
    19         
    20    initChart: ->
    21      @stage.removeAllChildren()    
    22         
    23      # other init chart code later
    24         
    25    drawChart: (value, refValue = 0) ->    
    26      # draw the chart code later
    

  2. When we init the chart, we create the rectangle shape and referencing line. They are put into this (@) scope for the drawChart to access.

    chart.coffee


     1  initChart: ->
     2      @stage.removeAllChildren()    
     3         
     4      margin = 5
     5         
     6      @shape = new RectShape
     7        fillColor: 'ORANGERED'
     8        width: @canvasWidth - margin
     9        height: 1
    10        x: margin
    11        y: @canvasHeight
    12      @stage.addChild @shape    
    13         
    14      @refLine = new RectShape
    15        fillColor: 'RED'
    16        width: @canvasWidth - margin
    17        height: 1
    18        x: margin
    19        y: @canvasHeight
    20      @stage.addChild @refLine
    

  3. We have drew the shape in the initChart method. The drawChart method is actually used to calculate the chart dimension based on the provided value. At last, we animate the shape to the target position and dimension by using TweenJS.

    chart.coffee


     1  drawChart: (value, refValue = 0) ->    
     2      areaForEachTile = 100
     3      scaleY = value / areaForEachTile
     4      y = @canvasHeight - scaleY
     5      cjs.Tween.get(@shape).to({scaleY, y}, 400, Ease.quartOut)
     6         
     7      refY = @canvasHeight - (refValue / areaForEachTile)
     8      cjs.Tween.get(@refLine).to({y: refY}, 400, Ease.quartOut)
     9         
    10           
    11      # Draw on canvas
    12      @stage.update()
    

  4. We changed the drawChart method arguments. Now we need the reference value for positioning the reference line. So we update the view logic where it toggles the drawChart method.

    view.coffee


     1  this.app.handleListChange = (chart1, chart2)->
     2    # Toggle Chart 1 and 2
     3    $('input[type="radio"], input[type="checkbox"]').change ->
     4      # Chart 1
     5      value = $('input[type="radio"]:checked').val()
     6      $('.output1').text(Math.round(value))
     7      chart1.drawChart(value)      
     8         
     9      # Chart 2
    10      sum = 0
    11      $('input[type="checkbox"]:checked').each ->
    12        sum += $(this).val()*1
    13      $('.output2').text(Math.round(sum))
    14      chart2.drawChart(sum, value)
    

What just happened?

We used the TweenJS the first time. Here is the syntax:

1 craetejs.Tween.get(anyObject).to({property:newValue, }, duration, easeFunction)

Animated tiles chart

  1. Similar to the last example, we prepare the chart.coffee file with the basic structure: constructor, initChart and drawChart.

    chart.coffee


     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5         
     6      cjs.Ticker.setFPS(60)
     7      cjs.Ticker.addEventListener 'tick', @stage
     8             
     9      utility.retinalize @stage, false
    10      @canvasWidth = utility.originalCanvasWidth
    11      @canvasHeight = utility.originalCanvasHeight
    12         
    13      @initChart()
    14      @lastNumberOfTiles = 0
    15       
    16 
    17    initChart: ->
    18      @stage.removeAllChildren()    
    19         
    20      # Init code later
    21           
    22 
    23    drawChart: (value) ->  
    24         
    25      # Code later
    

  2. The initChart method will draw all the tiles in the canvas area. By default all the tiles has 0 scaling so they are not visible at the beginning.

    chart.coffee


     1  initChart: ->
     2    @stage.removeAllChildren()    
     3       
     4    tileWidth = tileHeight = 10
     5    margin = 5
     6    leadingMargin = margin + tileWidth / 2
     7       
     8    chartArea = (@canvasWidth-margin-leadingMargin) * (@canvasHeight-margin-leadi\
     9 ngMargin)
    10    tileArea = (tileWidth + margin) * (tileHeight + margin)    
    11    @maxNumberOfTiles = Math.floor( chartArea / tileArea )    
    12       
    13    cols = Math.floor((@canvasWidth-margin) / (tileWidth + margin))
    14       
    15    @shapes = []
    16    for i in [0...@maxNumberOfTiles]      
    17      x = leadingMargin + Math.floor(i % cols) * (tileWidth + margin)
    18      y = leadingMargin + Math.floor(i / cols) * (tileHeight + margin)
    19      shape = new RectShape
    20        fillColor: 'ORANGERED'
    21        width: tileWidth
    22        height: tileHeight      
    23        x: x
    24        y: y        
    25      @stage.addChild shape
    26      @shapes.push shape
    27         
    28      shape.regX = tileWidth/2
    29      shape.regY = tileHeight/2
    30      shape.scaleX = shape.scaleY = 0
    

  3. The drawChart method doesn’t draw the chart. It actually toggle the scaleX and scaleY of the existing tiles. It will scale up or down the tiles based on the given value.

    chart.coffee


     1  drawChart: (value) ->  
     2    # each tile = 100K km2
     3    areaForEachTile = 100
     4    numberOfTiles = Math.floor(value / areaForEachTile)    
     5    for i in [0...@maxNumberOfTiles]
     6      if i < numberOfTiles
     7        delay = (i - @lastNumberOfTiles) * 5
     8        Tween.get(@shapes[i]).wait(delay).to({scaleX:1, scaleY:1}, 400, Ease.quar\
     9 tOut)
    10      else
    11        delay = (i - @lastNumberOfTiles) * 2
    12        Tween.get(@shapes[i]).wait(delay).to({scaleX:0, scaleY:0}, 400, Ease.quar\
    13 tOut)
    14    @lastNumberOfTiles = numberOfTiles
    

  4. In the view.coffee, we ensure the logic to toggle both chart is correct as the following. It is the same as the project 4 code.

    view.coffee


     1  this.app.handleListChange = (chart1, chart2)->
     2    # Toggle Chart 1 
     3    $('input[type="radio"]').change ->
     4      value = $('input[type="radio"]:checked').val()
     5      $('.output1').text(Math.round(value))
     6      chart1.drawChart(value)  
     7 
     8    # Toggle Chart 2
     9    $('input[type="checkbox"]').change ->
    10      sum = 0
    11      $('input[type="checkbox"]:checked').each ->
    12        sum += $(this).val()*1
    13      $('.output2').text(Math.round(sum))
    14      chart2.drawChart(sum)
    

What just happened?

We have created a nice animated tile-based bar chart.

Static pie chart

  1. There is only 1 pie chart. We change the HTML to contain 1 chart only.

    index.html


    1  <div class='charts'>
    2    <canvas id="chart-canvas" width="150" height="150"></canvas>
    3  </div>
    

  2. We also center align the canvas.

    app.css


    1  #chart-canvas {
    2    display: block;
    3    margin: auto;
    4  }
    

  3. The pie chart is different from the bar chart we have created.

    chart.coffee


     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5      cjs.Ticker.setFPS(60);
     6      cjs.Ticker.addEventListener 'tick', @stage
     7         
     8      utility.retinalize @stage, false
     9      @canvasWidth = utility.originalCanvasWidth
    10      @canvasHeight = utility.originalCanvasHeight    
    11         
    12      @initChart()    
    13         
    14    initChart: ->
    15      @stage.removeAllChildren()    
    16         
    17         
    18    drawChart: (leftValue, rightValue) ->   
    19      x = @canvasWidth / 2
    20      y = @canvasHeight / 2
    21      r = 50
    22         
    23      globalRotation = -90 * Math.PI / 180
    24         
    25      percentage = rightValue / (leftValue + rightValue)
    26      splitDegree = percentage * 360
    27         
    28      #Arc 1
    29      startAngle = 0 * Math.PI / 180 + globalRotation
    30      endAngle = splitDegree * Math.PI / 180 + globalRotation       
    31         
    32      shape = new cjs.Shape()
    33      shape.graphics
    34        .beginFill "GOLD"
    35        .moveTo(x, y)
    36        .arc(x, y, r, startAngle, endAngle)
    37        .lineTo(x, y)
    38           
    39      @stage.addChild shape
    40         
    41      # Arc 2
    42      startAngle = splitDegree * Math.PI / 180 + globalRotation
    43      endAngle = 360 * Math.PI / 180 + globalRotation 
    44         
    45      shape = new cjs.Shape()
    46      shape.graphics
    47        .beginFill "ORANGERED"
    48        .moveTo(x, y)
    49        .arc(x, y, r, startAngle, endAngle)
    50        .lineTo(x, y)
    51           
    52      @stage.addChild shape
    

  4. We changed to 1 chart, so we also change the handleListChange method to toggle the chart with both values form left and right list.

    view.coffee


     1  this.app.handleListChange = (chart)->
     2    # Toggle Chart
     3    $('input[type="radio"], input[type="checkbox"]').change ->
     4      # Left
     5      value = $('input[type="radio"]:checked').val()*1
     6      $('.output1').text(Math.round(value))
     7 
     8      # Right
     9      sum = 0
    10      $('input[type="checkbox"]:checked').each ->
    11        sum += $(this).val()*1
    12      $('.output2').text(Math.round(sum))
    13         
    14      # Update Chart
    15      chart.drawChart(value, sum)
    

Animated pie chart

  1. The animated chart.

    chart.coffee


     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5      cjs.Ticker.setFPS(60);
     6      cjs.Ticker.addEventListener 'tick', @stage
     7         
     8      utility.retinalize @stage
     9      @canvasWidth = utility.originalCanvasWidth
    10      @canvasHeight = utility.originalCanvasHeight    
    11         
    12      @initChart()    
    13         
    14    initChart: ->    
    15      @pieData = {
    16        splitDegree: 0 
    17      }    
    18 
    19    updateChart: (e) =>      
    20      # Code to draw arc shape later
    21 
    22    drawChart: (leftValue, rightValue) ->       
    23      # Code to start the shape drawing later
    

  2. In the updateChart method, we clear the stage and draw the arc again based on the current splitDegree.

    chart.coffee


     1  updateChart: (e) =>      
     2    x = @canvasWidth / 2
     3    y = @canvasHeight / 2
     4    r = 50
     5    globalRotation = -90 * Math.PI / 180
     6       
     7    @stage.removeAllChildren()
     8       
     9    #Arc 1
    10    startAngle = 0 * Math.PI / 180 + globalRotation
    11    endAngle = @pieData.splitDegree * Math.PI / 180 + globalRotation       
    12       
    13    shape = new cjs.Shape()
    14    shape.graphics
    15      .beginFill "GOLD"
    16      .moveTo(x, y)
    17      .arc(x, y, r, startAngle, endAngle)
    18      .lineTo(x, y)
    19         
    20    @stage.addChild shape
    21       
    22    # Arc 2
    23    startAngle = @pieData.splitDegree * Math.PI / 180 + globalRotation
    24    endAngle = 360 * Math.PI / 180 + globalRotation 
    25       
    26    shape = new cjs.Shape()
    27    shape.graphics
    28      .beginFill "ORANGERED"
    29      .moveTo(x, y)
    30      .arc(x, y, r, startAngle, endAngle)
    31      .lineTo(x, y)
    32         
    33    @stage.addChild shape
    

  3. The splitDegree value is actually changing to create the animated effect.

    chart.coffee


    1  drawChart: (leftValue, rightValue) ->       
    2    percentage = rightValue / (leftValue + rightValue)
    3    splitDegree = percentage * 360
    4       
    5    Tween.get(@pieData).to({splitDegree}, 400, Ease.quantOut).addEventListener('c\
    6 hange', @updateChart)
    

What just happened?

We used the TweenJS to animate the pieData object. TweenJS is an independent library that the target is not necessary to be any CreateJS display object. We can provide any object and ask the TweenJS to tween any numeric property. In the change event, we know the changes happened so we can updated our canvas in our own way. In this example, we create a new pie chart based on the changing value.

Summary

Drawing chart is one of the common canvas usage.

Further challenges

We have discussed the usage of gyroscope sensor in the chapter Rain or Not. What if combine what I have learned there with the chart drawing together? Try creating an inspector as following that shows the value history of the sensor.

Rotation Inspector

Rotation Inspector

You can test the real application by using the following links with your devices.

  • Gyroscope Rotation: http://mztests.herokuapp.com/rotation/
  • Accelerometer: http://mztests.herokuapp.com/motion

Optionally, you may download the app in the Play Store.

https://play.google.com/store/apps/details?id=net.makzan.gyroinspcetor&hl=en

The rotation value ranged from -365 to +365. When we use the following chart, the rotation value shows as a history for better inspection.

Chart explanation

Chart explanation

Change Log

Version: 2015-01-09

The schedule of the book is a weekly release for new chapters. Then it will get edited and done in early 2015. The following is the change log of this book.

Todos

  • Add chapter 1b explanation.
  • Add chapter 2 explanation.
  • Split chapter 3 into steps.

What have done

  • 2015-01-09
    • Added project 4B summary.
  • 2015-01-03
    • Added chapter 4B code example, which draws several different charts based on the same data in chapter 4.
  • 2014-12-27
    • Added chapter 4 code example, which draws a interactive chart based on the user selection.
  • 2014-12-13
    • Added chapter 3 code example, which demonstrated a solar system app with parallax effects.
    • Updated explanation in previous chapters.
  • 2014-12-06
    • Added chapter 2 code example, which separates code into modules by the modal-view-controller pattern.
  • 2014-11-22
    • Added chapter 1b example, which makes use of the Canvas transition in DOM-based application.
  • 2014-11-07
    • Improved chapter 1 explanation.
  • 2014-10-26
    • Added CreateJS introduction.
    • Organized the book content with front matters and 2 parts of content.
    • Added chapter 1 – basic app with animated transitions
  • 2014-10-19
    • Added changes log to the book.
  • 2014-10-18
    • Initial the book with pre-request section.

Thanks again for reading the book. Hope you enjoy the example porjects and learn to build mobile app with the CreateJS library.

If you find any questions, you’re welcome to contact me for any questions. You can find me at:

  • Website: makzan.net
  • Email: mak@makzan.net
  • Twitter: @makzan