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.htmlwe had the canvas inside the#appDIV. We will add new DOM elements after this canvas tag, for each page of content.index.html 1<divid="app">2<canvasid="app-canvas"width="300"height="400"></canvas>3<!--Wewilladdeachpageofcontentfromhere-->4</div> - First, we add the main menu page. Add the following
#mainDIV after our canvas.index.html 1<divid="app">2<canvasid="app-canvas"width="300"height="400"></canvas>3<divid="main"class="page">4<divclass='header'>5<imgsrc="images/header.png"alt="Header">6</div>7<ulid="main-list"class='non-collapse-content'>8<li><ahref="#detail-page"><imgsrc='images/a.png'alt='Photo A'></a></li>9<li><ahref="#detail-page"><imgsrc='images/b.png'alt='Photo B'></a></li>10<li><ahref="#detail-page"><imgsrc='images/c.png'alt='Photo C'></a></li>11<!--Feelfreetoaddmorelistitemshere-->12</ul>13<divid="info-link">14<ahref="#info-page"data-transition="TransitionAnimationA"><imgsrc='ima\15ges/info.png'alt='Link to Info'></a>16</div>17</div>18</div> - Next, we add the
#detail-pageafter the mail page. You may add other page if needed.index.html 1<divid="app">2<canvasid="app-canvas"width="300"height="400"></canvas>3<divid="main"class="page">...</div>4<divid="detail-page"class="page">5<ahref="#"class='header'><imgsrc='images/header-back.png'alt='Back to m\6ain'></a>7<imgsrc='images/photo-a.png'alt='Detail of Photo A'>8<p>HereisaphotofromUnsplash.Thephotoisfreeforcommercialuse.Ip\9utitherejustfortheappexample.ThephotowastakenbyBenMoore.</p>10<divid="info-link">11<ahref="#info-page"data-transition="TransitionAnimationA"><imgsrc='ima\12ges/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<divid="app">2<canvasid="app-canvas"width="300"height="400"></canvas>3<divid="main"class="page">...</div>4<divid="detail-page"class="page">...</div>5<divid="info-page"class="page">6<ahref="#"class='header'><imgsrc='images/header-back.png'alt='Back to m\7ain'></a>8<divclass='non-collapse-content'>9<p>Thisisanexampleappthatservesasthe1stchapterofmybook–Ric\10hInteractiveAppDevelopmentwithCreateJS.Thisexampledemonstratesacustom\11animatedtransition.Itlackssomeessentialfeaturesbutthisisjustforthec\12hapter1.Morefeaturescominginfuturechapter.</p>13<p>ThisexampleisboughttoyoubyMakzan.Hehaswrittenthreebooksan\14donevideocourseonbuildingaFlashvirtualworldandcreatinggameswithHTM\15L5andthelatestwebstandards.HeiscurrentlyteachingcoursesinHongKonga\16ndMacaoSAR.</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 1ul{2list-style:none;3}45img{6width:100%;7border:0;8}910/* canvases sit inside the #app frame. It’s similar to layers. */11#app{12position:relative;13}14#app>canvas{15position:fixed;16display:none;/* default hide until we use it */17z-index:999;18}
What just happened?
We are building the project of Jack Portfolio from scratch. We use DIV with .page class to indicate one page of content. They are all added into #app element.
Here is the new #app DOM structure.
1 <div id="app">
2 <canvas id="app-canvas" width="300" height="400"></canvas>
3 <div id="main" class="page">...</div>
4 <div id="detail-page" class="page">...</div>
5 <div id="info-page" class="page">...</div>
6 </div>
The HTML is designed to be used without any JavaScript and fancy transition effect. All the links between content are based on the hash anchors. User can still view and link to different part of the content with neither canvas support nor JavaScript support.
2. Page Transition Manager
In this step, we control our .page DOM elements by a PageManager.
Preparation
We need to modify the scenes management. It was controlling the DisplayObject in CreateJS canvas. Now we need to control the DOM elements. I changed the class from SceneManager to PageManager to make the code less confusing. The structure will be the same as what we had in canvas-based scenes manager.
1 class app.PageManager
2 constructor: (@stage)->
3 # init the pages
4 lastScene: ->
5 # return last scene
6 resetWithScene: (scene) ->
7 # reset scene
8 popScene: ->
9 # remove the last scene
10 pushScene: (scene)->
11 # show the given scene
12 pushSceneWithTransition: (scene, transitionClassName) ->
13 # show the given scene with transition
Time for Action
- 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{3position:relative;4}5#app>canvas{6position:fixed;7display:none;/* default hide until we use it */8z-index:999;9}1011/* Page related */12.page{13width:100%;14height:100%;15position:absolute;16} - By default, we hide all the
.page. The first page will be added from theresetWithScenemethod.page-manager.coffee 1constructor:(@stage)->2@scenes=[]3$('.page').hide()45#registerclicksonallpages6$('a[href^="#"]').click(event)=>7pageId=$(event.currentTarget).attr('href')89#whenit's link to #, it is a back transition10pageId = '.page:first' if pageId == '#'1112transition = $(event.currentTarget).data('transition')1314@pushSceneWithTransition$(pageId),transition - The
lastScenereturns the last element of thescenesarray.page-manager.coffee 1lastScene:->@scenes[@scenes.length-1] - In the
resetScenemethod, we reset the scenes array and use jQuery to show the given scene.page-manager.coffee 1resetWithScene:(scene)->2@scenes.length=03@scenes.pushscene45$(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 1popScene:->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 1pushScene:(scene)->2$(@lastScene()).hide()3@scenes.pushscene4$(scene).show()56#resetthescroll7$(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.
page-manager.coffee 1pushSceneWithTransition:(scene,transitionClassName='TransitionAnimationB')\2->3transition=newlib[transitionClassName]()45#ThedemensionfollowstheFlashcanvasdimension6transition.x=300/27transition.y=400/28$('#app-canvas').show()910transition.on'sceneShouldChange',=>11@pushScenescene1213transition.on'transitionEnded',->14$('#app-canvas').hide()1516@stage.addChildtransition - Let’s make use of the page transition. We change the
app.coffeeto the following code.app.coffee 1#aglobalappobject.2this.exampleApp?={}34#alias5cjs=createjs6setting=this.exampleApp.setting7app=this.exampleApp89classApp10#Entrypoint.11constructor:->12console.log"Welcome to my portfolio."1314@canvas=document.getElementById("app-canvas")15@stage=newcjs.Stage(@canvas)1617cjs.Ticker.setFPS6018cjs.Ticker.addEventListener"tick",@stage#makesurethestagerefreshdr\19awingforeveryframe.2021app.sceneManager=newapp.PageManager(@stage)22app.sceneManager.resetWithScene$('.page:first')2324newApp()
What just happened?
We created the page transition manager that is very similar to our previous scene manager. The only difference is that page manager handles .page DOM element and scene manager handles CreateJS container.
Improvement
We have reset the scroll position during scenes transition. In future, we may store the scroll position of each scene, so that we can resume the previous scroll position when popping to the last scene.
3. Positioning the canvas transition
Time for Action
- In the
retinalize.coffeefile, we add asetFullScreenfunction that scales the canvas to fit the window dimension.retinalize.coffee 1#aglobalappobject.2this.exampleApp?={}34setting=this.exampleApp.setting56this.utility?={}78this.utility.setFullScreen=(canvas,stage)->9canvas.setAttribute'width',$(window).width()10canvas.setAttribute'height',$(window).height()11setting.width=$(window).width()12setting.height=$(window).height()1314#300istheoriginalFlashcanvaswidth15stage.scaleX=stage.scaleY=setting.width/300 - In our app’s entry point, we invoke the
setFullScreenfunction for the first time and register it to be run every time when the window resizes.app.coffee 1classApp2#Entrypoint.3constructor:->4console.log"Welcome to my portfolio."56@canvas=document.getElementById("app-canvas")7@stage=newcjs.Stage(@canvas)89utility.setFullScreen(@canvas,@stage)1011window.onresize==>12utility.setFullScreen(@canvas,@stage)1314...
4. Falling back in old browser
Time for Action
- We add a new file
old-browser.jsto 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 supported3isCanvas2DSupported=!!window.CanvasRenderingContext2D;45// Give up all logic6if(!isCanvas2DSupported){7// remove .page styles8$('.page').removeClass();9}10}).call(this); - We include the
old-browser.jsfile right after the loading of jQuery and before loading our logic.index.html 1<scriptsrc="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{2position:fixed;3bottom:-5px;4left:0;5width:100%;6}78.header{9position:fixed;10top:0;11left:0;12width:100%;13}1415.non-collapse-content{16margin-top:25%;17}1819p{20padding:1rem;21}2223#main-list{24padding-bottom:100px;25}26#main-listli{27margin-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 thesrcsetattribute for everyimgtag.index.html 1<imgsrc="images/header.png"srcset='images/header.png 1x, images/header@2x.png\22x'alt="Header">3...4<li><ahref="#detail-page"><imgsrc='images/a.png'srcset='images/a.png 1x, ima\5ges/a@2x.png 2x'alt='Photo A'></a></li>6<li><ahref="#detail-page"><imgsrc='images/b.png'srcset='images/b.png 1x, ima\7ges/b@2x.png 2x'alt='Photo B'></a></li>8<li><ahref="#detail-page"><imgsrc='images/c.png'srcset='images/c.png 1x, ima\9ges/c@2x.png 2x'alt='Photo C'></a></li>10...11<imgsrc='images/info.png'srcset='images/info.png 1x, images/info@2x.png 2x'a\12lt='Link to Info'>13...14<imgsrc='images/header-back.png'srcset='images/header-back.png 1x, images/hea\15der-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=''>