“CreateJS是基于HTML5开发的一套模块化的库和工具,拥有共同或独立工作的丰富交互式内容的开源Web技术。” CreateJs由几个库组成:

1
2
3
4
5
createjs
├── easeljs //核心代码库,如分层显示列表、交互模型、事件机制
├── tweenjs //动画缓动函数
├── preloadjs //预加载方案
└── (soundjs) //与音频有关,简单的背景音乐直接用audio标签就好咯

而Flash CC导Canvas,实质就是导出CreateJs动画,另外通常还会引用一个movieClip的插件,其名曰影片剪辑,页面代码大致长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- demo.html -->

<!-- 引用createjs资源 -->
<script src="http://code.createjs.com/easeljs-0.7.1.min.js"></script>
<script src="http://code.createjs.com/tweenjs-0.5.1.min.js"></script>
<script src="http://code.createjs.com/movieclip-0.7.1.min.js"></script>
<script src="http://code.createjs.com/preloadjs-0.4.1.min.js"></script>

<!-- 动画核心代码 -->
<script src="demo.js"></script>

<!-- 舞台初始化和动画执行 -->
<script>
var canvas, stage, exportRoot;

function init() {
canvas = document.getElementById("canvas");
images = images||{};

var loader = new createjs.LoadQueue(false);
loader.addEventListener("fileload", handleFileLoad);
loader.addEventListener("complete", handleComplete);
loader.loadManifest(lib.properties.manifest);
}

function handleFileLoad(evt) {
if (evt.item.type == "image") { images[evt.item.id] = evt.result; }
}

function handleComplete() {
exportRoot = new lib.demo();

stage = new createjs.Stage(canvas);
stage.addChild(exportRoot);
stage.update();

createjs.Ticker.setFPS(lib.properties.fps);

//定时更新画布
createjs.Ticker.addEventListener("tick", stage);
}
</script>


<canvas id="canvas" width="640" height="1280"></canvas>

<script>
$(function(){
init();
})
</script>

Adobe还是很牛逼的,在Flash CC中做好动画后,直接帮我们导出了上面的页面和相关的资源,我们需要做的就是对canvas画布做一下定位和必要的适配,目录如下:

1
2
3
4
demo
├── demo.html
├── demo.js
└── images

一、Flash导Canvas与外部的交互通信

在flash导出的canvas(html5 canvas画布,写的代码不是AS了,而是咱们熟悉的js)中,你可以随便调用外部的变量和函数,而你要调用flash导出的canvas里面的方法,canvas需要将变量和函数作为类的属性和方法暴露出去供外部调用。看这行代码你就懂了:

1
exportRoot = new lib.demo();

Canvas里所有的元素都是exportRoot的子元素。你可以在flash中的代码区中,像this.isReady = function(){} 或者 exportRoot.isReady = function(){} 这样来暴露一个方法,外部调用这个方法,exportRoot.isReady() 这样就可以了。如此,内外交互通信就没问题了。还有一些flash导出的canvas已经自带的常用方法供外部调用:

1
2
3
4
exportRoot.play()    //动画播放
exportRoot.stop() //动画暂停
exportRoot.gotoAndPlay(100) //跳到动画100帧继续执行
exportRoot.gotoAndStop(100) //跳到动画100帧停止


上图中产生的交互行为是这样的:canvas动画走到这里会执行this.stop()停止,并调用外部方法 showBtn() 和 miShow() 让点击按钮和飞船logo点击层出现,用户在完成它的点击操作后,外部会执行 exportRoot.play() 和 exportRoot.barndEgg.gotoAndPlay(1) 让动画继续,还有让彩蛋+1动画执行。exportRoot.barndEgg.gotoAndPlay(1)的意思是,彩蛋影片剪辑也是有自己的动画时间轴的,从他的第一帧播放,就把这个彩蛋单独的动画播放出来了。同样,canvas动画在开始时和结束时,想要做什么操作,也十分方便了。

接下来,我们要说说导出的canvas一个很重要的全局函数init(),它里面包含了资源的预加载、画布的初始化和动画的渲染执行,那就意味着,页面一加载就应该执行init()。如此一来,咱们得在flash中动画的第一帧设置为暂停,我们需要执行动画的时候再手动执行。

二、CreateJs在jShop移动端的优化

Flash导出的Canvas代码在PC端放上去基本上就可以完事了,但是在移动端绝不是如此,尤其是在jShop移动端。

1、避免画布重绘

移动端需要对canvas画布大小进行适配,避免使用宽度100%或者是通过js判断来设置改变宽度,会引起重绘,改由css3 transform进行缩放。

或者利用createjs.Container(),Container是一个容器类,对这个容器所作的操作可以影响到它所包含的所有元素,而且表现跟css3 trasnform一样不会引起重绘制,我们可以改写一下handleComplete()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function handleComplete() {
exportRoot = new lib.gonglue618();
stage = new createjs.Stage(canvas);

// 类似一个中间件一样
var container = new createjs.Container();
container.addChild(exportRoot);

...//container.scale=xxx,对container进行缩放操作

stage.addChild(container);
stage.update();
stage.enableMouseOver();
createjs.Ticker.setFPS(lib.properties.fps);

//定时更新画布,我可能不希望执行回调就更新画布,后面再说
//createjs.Ticker.addEventListener("tick", stage);
}

2、jShop按模块异步加载,某些代码可能执行失效。

页面代码大致用以下的图表示,分开4个自定义模块来写,因为做了预加载处理(两层预加载,自己的外部DOM资源预加载和Canvas动画自身的资源预加载),每个自定义模块都大胆勾选了打开页面就加载。一方面方便维护,一方面避免单个模块代码量太大异步加载失败和避免jShop的编辑器挂掉。

异步加载,就意味着每一块代码加载完成的速度是不一样的,但不代表谁的代码少谁加载的就快,它跟时刻不稳定的网络环境等等因素有关。因此,在jShop中,js要确保其所依赖的js或者dom加载完才能调用。

举个栗子,一开始我的Canvas动画代码像下面那样就有问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...//动画核心代码

<script>
var canvas, stage, exportRoot;

function init() {
canvas = document.getElementById("canvas");
images = images||{};

var loader = new createjs.LoadQueue(false);
loader.addEventListener("fileload", handleFileLoad);
loader.addEventListener("complete", handleComplete);
loader.loadManifest(lib.properties.manifest);
}

...//handleFileLoad、handleComplete
</script>


<script>
$(function(){
init();
})
</script>

canvas标签在DOM结构模块,它有可能还没加载出来,canvas画布就获取不到了,init执行就会报错,动画永远也不会出来,我们做个变化,把init放在DOM结构模块中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<canvas id="canvas" width="720" height="1280"></canvas>
...

<script>
$(function(){
var initTimer = setInterval(function(){
if(init){
clearInterval(initTimer);
init();
}
}, 1000)
})
</script>

它们在同一个模块,可以保证init执行的时候,canvas元素已存在。另外,我们还需要通过检测init是否已存在来保证canvas动画模块(稍微复杂一些的动画,这一块的代码就非常的大)已经加载完成,再调用init。那么需要检测CreateJs库是否已经加载完吗?经过反复测试,库的代码并不大,而且在页面各模块的上头,加载可以认为瞬间完成,无须检测。

再举个栗子,在业务JS中,像下面这样给元素绑定事件也会出现问题:

1
2
3
4
5
6
7
$('.fengmian-btn').on('click', function(){
if($(this).hasClass('canTouch')){
//执行动画
canvasAnimation();
shareFlag = false;
}
})

由于DOM结构有可能还没有加载出来,这样绑定就失去意义了,任凭你怎么点击,都不会出现你想要的结果。因此,我们需要进行事件委托,将事件绑定到body上,检测事件的target,如果与传入的选择器匹配,就触发事件,否则不触发:

1
2
3
4
5
6
7
$('body').on('click', '.fengmian-btn', function(e){
if($('.fengmian-btn-wrap').hasClass('canTouch')){
//执行动画
canvasAnimation();
shareFlag = false;
}
})

3、CreateJs定时更新画布的性能问题

下面这个函数是CreateJs资源准备完毕后执行的回调,做初始化画布和定时更新画布的工作,因为我们在Canvas动画的第一帧已经设置了动画暂停,因此,动画并没有向前走。

1
2
3
4
5
6
7
8
9
10
11
12
function handleComplete() {
exportRoot = new lib.demo();

stage = new createjs.Stage(canvas);
stage.addChild(exportRoot);
stage.update();

createjs.Ticker.setFPS(lib.properties.fps);

//定时更新画布
createjs.Ticker.addEventListener("tick", stage);
}

但是,createjs.Ticker.addEventListener(“tick”, stage) 这个计时器在IOS上很危险。IOS拥有出色的运行效率,给人非常流畅的体验,有赖于起出色的资源回收机制,当应用进入后台时,IOS会暂停其所有线程,回收内存以运行其他应用,所以,当你重新激活原本界面时,所有工作从当时退出的状态继续执行。

既然是这样,CreateJS的动画渲染线程理应也是会停止的。但从表现来看,一旦动画进入后台,IOS的CPU负荷过大,iphone 5s以下尤其明显,CPU很可能会处理不过来,在重新激活界面时,动画基本卡停,甚至造成应用崩溃。关于createjs.Ticker.addEventListener(“tick”, stage)背后是如何工作的,还有IOS的资源绘制机制,还不是很了解,如果大家知道,欢迎教教我。

临时能做的是,动画暂停的时候 createjs.Ticker.removeEventListener(“tick”, stage),动画需要播放的时候才更新画布; 另外,页面挂起和激活的状态下,我们也得手动操作。

1
2
3
4
5
6
7
8
9
10
11
// 页面挂起的时候停止更新画布
window.addEventListener("pause", function(){
exportRoot.stop();
createjs.Ticker.removeEventListener("tick", stage);
}, false);

// 页面激活的时候重新更新画布
window.addEventListener("resume", function(){
createjs.Ticker.addEventListener("tick", stage);
exportRoot.play();
}, false);

完成这些改动之后,在重新激活页面的时候,动画的卡停和崩溃问题解决了,然后动画还有可能会很卡。页面挂起和重新激活这个情况,由于用户发生了分享行为的比重很大,由于做的游戏,衡量之后用了个取巧的办法,改变游戏规则:凡是产生游戏中途终止的行为,都视为游戏失败,去到最后的分享页,因为游戏结束进入结算的时候,把画布清除了,渲染也停了,此后随便分享随便挂起,页面自然不会有问题。

因此,尝试监听pause和resume去做游戏中途终止的处理,发现失败了,我对这两个pause和resume事件也很陌生。无奈之下,利用页面挂起JS线程暂停来模仿页面重新被唤起的的状态:

1
2
3
4
5
6
7
8
9
10
11
12
var nowTime = new Date().getTime(), newTime = null, diffTime = null;
var resumeTimer = setInterval(function(){
newTime = new Date().getTime();
diffTime = newTime - nowTime;
if(diffTime>1000 && !shareFlag) {
canvasEnd();//动画结束处理去到结算页
$('.endpage2-text1,.endpage2-text3,.caidanBrand-list,.caidanBrand-list2').hide();
$('.endpage2-text2,.endpage2-text4').show();
clearIntervel(resumeTimer);
}
nowTime = newTime;
}, 500)


没办法中的办法,欢迎大家支招CreateJs移动端的性能优化。