Table of Contents
- Preparation
-
Example projects
-
Project 1 – Basic app with animated transitions
- Mission Checklist
- Project Preparation
- 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 for retina display
- Further challenges
- Project 1B – DOM-based app with animated transitions
- Project 2 – Rain or Not?
- Project 3 – Solar System
-
Project 4 – Countries Area
- Why is this project awesome?
- Preparing the project
- Step 1 – Building the app layout with flex
- Step 2 – Listing the countries data
- Step 3 – Basic list selection and calculation
- Step 4 – Styling the radio and checkbox list
- Step 5 – Drawing the chart
- Step 6 – Adding the info panel and global app style
- Summary
- Project 4B – Drawing charts on canvas
- Change Log
-
Project 1 – Basic app with animated transitions
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.
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.
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 |
- 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.
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.
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.
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
);
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.
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.
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.
Mission Checklist
We are going to build the portfolio project by following these tasks.
- Setting up the project and GulpJS.
- Setting up the canvas and CreateJS library.
- Defining scene as container inheritance.
- Adding static stylish menu.
- Displaying another scene after menu selection.
- Animating transition between scenes.
- 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.
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.
- 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
- We can generate the
package.json
file via thenpm init
method. After we go through thenpm 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
- 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
}
- 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 theGulpfile.js
and include the.coffee
version of the GulpFile.gulp.js
1
require
(
'coffee-script/register'
);
2
require
(
'./Gulpfile.coffee'
);
- 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 thejs
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'
]
- We create 2 CoffeeScript files to see if our
Gulpfile
works. They areapp.coffee
andsetting.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"
- Now we can run
gulp
in the terminal and we should see anapp.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 |
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
Time for Action
Let’s follow the following steps to setup our canvas and CreateJS library.
- 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>
- 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
:
400
px
;
19
width
:
300
px
;
20
margin
-
top
:
-
200
px
;
21
margin
-
left
:
-
150
px
;
22
}
- 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
}
- 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
()
- 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 |
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
:
400
px
;
6
width
:
300
px
;
7
margin
-
top
:
-
200
px
;
8
margin
-
left
:
-
150
px
;
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.
- 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
- 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
- We have created a new file
scenes.coffee
, we need to include it into the GulpJS pipeline. Add the file into thegulp.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
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.
- 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
- 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
- In the app logic, we replace the old
Scene
by the newly createdSceneA
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.
- 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
}
- We create more scenes to test our example. Add the
SceneB
to thescenes.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
()
- 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
()
- 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
- 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
- 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
- In the main
App
logic, We removed the old Scene creation logic and make use of thesceneManager
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
- 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.
- In the
scene-manager.coffee
, we add one new methodpushSceneWithTransition
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
}
- In the
scenes.coffee
, we change to use the newpushSceneWithTransition
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.
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.
- The
retinalize
method is kind of utility that’s independent to our logic. We create a new fileutility.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
- 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
)
- 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.
- Defining DOM elements
- Controlling page-based scenes.
- Positioning the canvas-based transition to fit window size.
- Falling back for old browser without canvas support.
- Fine tuning the app styles.
- 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
- 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>
- 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>
- 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>
- 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
1
st
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>
- 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
- 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
}
- By default, we hide all the
.page
. The first page will be added from theresetWithScene
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
- The
lastScene
returns the last element of thescenes
array.page-manager.coffee
1
lastScene
:
->
@
scenes
[
@
scenes
.
length
-
1
]
- 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
()
- 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
()
- 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
)
- 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
- 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
- In the
retinalize.coffee
file, we add asetFullScreen
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
- 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
- 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
);
- 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
- 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
:
-
5
px
;
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
:
1
rem
;
21
}
22
23
#
main
-
list
{
24
padding
-
bottom
:
100
px
;
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
- In the
index.html
, We add thesrcset
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.
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.
- Setup the project
- Data
- Mocking API
- View
- Adding Canvas
- Moving the Canvas in
- Aligning with FlexBox
- 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
- 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>
- 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
)
- 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
- 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
- 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
:
#
9
FB6C4
;
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
.
75
s
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
- 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.
- 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}"
)
- 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
- 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
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
:
50
px
;
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
:
13000
px
;
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.5
s
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
:
80
px
;
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
*
2
px
108
*
2
px
150
*
2
px
228
*
2
px
779
*
2
px
1430
*
2
px
2880
*
2
px
4500
*
2
px
;
3
$x
-
positions
:
30
vw
15
vw
45
vw
50
vw
35
vw
60
vw
25
vw
56
vw
;
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 |
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
}
+
40
vh
));
10
}
11
.
bg
.
layer
{
12
transform
:
translateY
(
calc
((
-
#
{
$space
-
height
}
+
#
{
$distance
}
+
40
vh
)
/
#
{
$
\
13
bg
-
z
}));
14
}
15
.
deep
-
bg
.
layer
{
16
transform
:
translateY
(
calc
((
-
#
{
$space
-
height
}
+
#
{
$distance
}
+
40
vh
)
/
#
{
$
\
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.
- Building the app layout with flex
- Listing the countries data
- Basic list selection and calculation
- Styling the radio and checkbox list
- Drawing the chart
- Adding the info panel and global app style
Preparing the project
Before we get started the project, let’s prepare several files. They are:
- Gulpfile.coffee
- retinalize.coffee
- 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.
- 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>
- 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
:
1
px
solid
green
;
/* debug */
11
}
12
.
container
.
shrink
{
13
flex
:
0
1
auto
;
14
}
- 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
}
- 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
:
150
px
;
5
}
6
7
.
charts
{
8
min
-
height
:
150
px
;
9
}
10
11
/* Area Description */
12
p
.
description
{
13
min
-
height
:
30
px
;
14
}
- 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
:
5
px
;
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.
- 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>
- 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
...
- 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
()
- 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.
- 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
))
- 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
- 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
:
1
rem
;
11
padding
:
1
rem
.
5
rem
;
12
padding
-
left
:
2
rem
;
13
border
:
1
px
solid
transparent
;
14
border
-
left
:
0
;
15
border
-
right
:
0
;
16
transition
:
all
.
3
s
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
10
px
50
%
no
-
repeat
;
28
background
-
size
:
16
px
;
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
)
10
px
50
%
no
-
repeat
;
33
background
-
size
:
16
px
;
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
)
10
px
50
%
no
-
repeat
;
40
background
-
size
:
16
px
;
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
- 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>
- 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
=
10
K
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
()
- 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
)
- 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
- 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>
- 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
100
K
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>
- 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
:
44
px
;
9
height
:
44
px
;
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
}
- The
#info-panel
is full screen that rotate into the view in 3D.app.css
1
/* Info panel */
2
.
hidden
{
3
transform
:
rotateY
(
-
90
deg
);
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
.
3
s
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
:
.
5
em
;
32
}
33
p
.
more
-
space
{
34
margin
:
2
em
;
35
}
- 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
:
700
px
;
3
font
-
family
:
Verdana
,
sans
-
serif
;
4
font
-
size
:
12
px
;
5
6
padding
:
5
px
;
7
background
:
ORANGERED
;
8
}
- Finally, make sure we have removed the debugging style.
app.css
1
.
container
>
*
{
2
border
:
1
px
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
- Animated bar chart
- Animated tiles
- Pie chart
- 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.
- 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
- When we init the chart, we create the rectangle shape and referencing line. They are put into
this
(@
) scope for thedrawChart
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
- We have drew the shape in the
initChart
method. ThedrawChart
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
()
- 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 thedrawChart
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
- Similar to the last example, we prepare the
chart.coffee
file with the basic structure:constructor
,initChart
anddrawChart
.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
- 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
- The
drawChart
method doesn’t draw the chart. It actually toggle thescaleX
andscaleY
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
=
100
K
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
- 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
- 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>
- We also center align the canvas.
app.css
1
#
chart
-
canvas
{
2
display
:
block
;
3
margin
:
auto
;
4
}
- 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
- 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
- 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
- In the
updateChart
method, we clear the stage and draw the arc again based on the currentsplitDegree
.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
- 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.
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.
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