swfで作ったコンテンツをHTML5 Canvasに書き出すお勉強のその2です。
今回は座標制御について。
青いボタン(btn_start)を押すとスロット(slot_mc0)が回転して、ランダムな位置で停止するというサンプルです。スロットが回転中はボタンを押せないようにしています。これをCanvasで動くように書き換えます(赤字はインスタンス名)。swfサンプルはこちら。
スロット(slot_mc0)のMovieClipの構造はこんな感じです。4つの絵の入ったMovieClipが縦に2つ(img_mc0,img_mc1)並んでいて、全体をマスクしています。マスクの範囲から出てしまったら、もう一方のMovieClipの上に来るような仕組みです。(当然ながら、MovieClipはx=0で揃えてあります)。
で、ASはこんな感じ。まずはルートのタイムラインに書いてあるスクリプト。JSに変換する前提なので、型指定はしていません。
【AS:ルートのタイムライン】
var timer_msg;
//startボタン:使用可
useStartBtn();
function onOver_start(event){
var mc = event.currentTarget;
mc.alpha = 0.7;
}
function onOut_start(event){
var mc = event.currentTarget;
mc.alpha = 1;
}
function onClick_start(event){
//startボタン:使用不可
nouseStartBtn();
slot_mc0.setRandom();
slot_mc0.startTimer_spin(1000*(0+1),3);
slot_mc0.startSlot();
}
function stopSlot(mc){
//startボタン:使用不可
useStartBtn();
}
function useStartBtn(){
btn_start.addEventListener(MouseEvent.ROLL_OVER,onOver_start);
btn_start.addEventListener(MouseEvent.ROLL_OUT,onOut_start);
btn_start.addEventListener(MouseEvent.CLICK,onClick_start);
btn_start.buttonMode = true;
btn_start.alpha = 1;
}
function nouseStartBtn(){
btn_start.removeEventListener(MouseEvent.ROLL_OVER,onOver_start);
btn_start.removeEventListener(MouseEvent.ROLL_OUT,onOut_start);
btn_start.removeEventListener(MouseEvent.CLICK,onClick_start);
btn_start.buttonMode = false;
btn_start.alpha = 0.3;
}
スロット自体の処理は、AS版ではスロット(slot_mc0)MovieClipのタイムラインに書いています。
【AS:slot_mc0のタイムライン】
var array_pos = [0,110,220,330,440];
var speed_max = 30;
var flg_end = false;
var flg_stop = false;
var flg_remove = false;
var posNo = 0;
var imgNo = 0;
var speed;
var timer_spin;
var pos_stop;
//開始位置をランダムにする
function setRandom():void{
var pos_y = xRandomInt(0,img_mc0.height);
img_mc0.y = pos_y;
img_mc1.y = img_mc0.y-img_mc1.height;
//はみ出しチェック
ckeckFrameOut();
//スピード:初期化
speed = speed_max;
//停止フラグ:初期化
flg_stop = false;
flg_end = false;
flg_remove = false;
posNo = 0;
}
//回転
function spin(event){
img_mc0.y += speed;
img_mc1.y += speed;
if (flg_end){
checkEndPoint();
}
if (flg_stop){
checkStopPoint();
}
ckeckFrameOut();
if (flg_remove){
flg_remove = false;
//停止時の位置ずれ補正
img_mc0.y = pos_stop;
if (img_mc0.y < 0){
img_mc1.y = img_mc0.y + img_mc0.height;
} else {
img_mc1.y = img_mc0.y - img_mc0.height;
}
if (img_mc1.y == 0){
img_mc0.y = img_mc1.y - img_mc0.height;
}
this.removeEventListener(Event.ENTER_FRAME,spin);
var parentObj = this.parent;
parentObj.stopSlot(this);
}
}
//停止地点チェック
function checkEndPoint(){
var i;
var dis_mim = img_mc0.height;
var flg_check = false;
//---スロットをランダムにする
for (i=0;i<array_pos.length;i++){
if (img_mc0.y >= 0 && img_mc0.y <= array_pos[i]){
flg_check = true;
break;
} else if (img_mc0.y < 0 && img_mc0.y <= array_pos[i]*-1){
flg_check = true;
break;
} else {
flg_check = false;
}
}
if (flg_check){
var dis = Math.abs(array_pos[i] - Math.abs(img_mc0.y));
if (dis < dis_mim){
dis_mim = dis;
posNo = i;
}
}
var fugo = (img_mc0.y < 0) ? -1 :1;
pos_stop = array_pos[posNo] * fugo;
//絵柄番号:判定(img_mc0.yが+値の時は、img_mc1の位置が当たりになる)
imgNo = (pos_stop < 0) ? posNo :(array_pos.length-1) - posNo;
imgNo = (imgNo == 4) ? 0:imgNo;
flg_end = false;
var amari = dis_mim % speed;
if (amari > 0){
img_mc0.y += amari;
}
flg_stop = true;
}
//停止監視
function checkStopPoint(){
if (pos_stop == img_mc0.y){
flg_stop = false;
flg_remove = true;
} else {
var margin = Math.abs(Math.abs(pos_stop) - Math.abs(img_mc0.y));
if (margin < speed){
speed = margin;
}
}
}
//はみ出しチェック
function ckeckFrameOut(){
if (img_mc0.y >= img_mc0.height){
img_mc0.y = img_mc1.y - img_mc0.height;
} else if (img_mc1.y >= img_mc1.height){
img_mc1.y = img_mc0.y - img_mc1.height;
}
}
//ランダムな整数値を取得
function xRandomInt (nMin,nMax) {
// nMinからnMaxまでのランダムな整数を返す
var nRandomInt = Math.floor(Math.random()*(nMax-nMin+1))+nMin;
return nRandomInt;
}
//Timer開始
function startTimer_spin(interval,count){
timer_spin = new Timer(interval,count);
timer_spin.addEventListener(TimerEvent.TIMER,onCheckTimer_spin);
timer_spin.addEventListener(TimerEvent.TIMER_COMPLETE,onEndTimer_spin);
timer_spin.start();
}
function startSlot(){
this.addEventListener(Event.ENTER_FRAME,spin);
}
function onCheckTimer_spin(event){
var timerObj = event.currentTarget;
var remain_count = timerObj.repeatCount - timerObj.currentCount;
if (remain_count <= 3){
remain_count = (remain_count == 0) ? 1 :remain_count;
speed = Math.round(speed / remain_count);
}
}
//Timer停止
function onEndTimer_spin(event){
//終了フラグ:on
flg_end = true;
}
そして、これをCanvasで書き出すために、JavaScriptで書き直したものが下記です。こちらも同じくFlash CCのルートのタイムラインに書いています。Canvasでパブリッシュしたサンプルはこちら。
【Canvas版JavaScript】
//インスタンスへのMouseOver関連イベントはコレが必須。
stage.enableMouseOver();
var h_img_mc = this.slot_mc0.img_mc1.y - this.slot_mc0.img_mc0.y;
var speed_max = 30;
var array_pos = [0,110,220,330,440];
//startボタン:使用可
useStartBtn(this);
function onClick_start(event){
//startボタン:使用不可
nouseStartBtn(event.currentTarget.parent);
var mc = event.currentTarget;
var slot = mc.parent.slot_mc0;
slot.img_mc0.height = h_img_mc;
slot.img_mc1.height = h_img_mc;
setRandom(slot);
slot.startTime = createjs.Ticker.getTime(true);
startSlot(slot);
startTimer_spin(slot,1000*(0+1),3);
}
function onOver_start(event){
var mc = event.currentTarget;
mc.alpha = 0.7;
}
function onOut_start(event){
var mc = event.currentTarget;
mc.alpha = 1;
}
//開始位置をランダムにする
function setRandom(mc){
var pos_y = xRandomInt(0,mc.img_mc0.height);
mc.img_mc0.y = pos_y;
mc.img_mc1.y = mc.img_mc0.y-mc.img_mc1.height;
//はみ出しチェック
ckeckFrameOut(mc);
//スピード:初期化
mc.speed = speed_max;
//停止フラグ:初期化
mc.flg_stop = false;
mc.flg_end = false;
mc.flg_remove = false;
mc.posNo = 0;
mc.remain_count = 0;
}
//はみ出しチェック
function ckeckFrameOut(mc){
if (mc.img_mc0.y >= mc.img_mc0.height){
mc.img_mc0.y = mc.img_mc1.y - mc.img_mc0.height;
} else if (mc.img_mc1.y >= mc.img_mc1.height){
mc.img_mc1.y = mc.img_mc0.y - mc.img_mc1.height;
}
}
//ランダムな整数値を取得
function xRandomInt (nMin,nMax) {
// nMinからnMaxまでのランダムな整数を返す
var nRandomInt = Math.floor(Math.random()*(nMax-nMin+1))+nMin;
return nRandomInt;
}
//Timer開始
function startTimer_spin(mc,interval,count){
mc.remain_count = count;
mc.addEventListener("tick",onCheckTimer_spin);
}
function startSlot(mc){
mc.addEventListener("tick",spin);
}
function onCheckTimer_spin(event){
var mc = event.currentTarget;
var wCount = Math.floor((createjs.Ticker.getTime(true) - mc.startTime)/1000);
var count = 3 - wCount;
if (mc.remain_count == count){
return;
} else {
mc.remain_count = count;
if (mc.remain_count <= 3){
if (mc.remain_count == 0){
mc.removeEventListener("tick",onCheckTimer_spin);
onEndTimer_spin(mc);
} else {
mc.speed = Math.round(mc.speed / mc.remain_count);
}
}
}
}
//回転
function spin(event){
var mc = event.currentTarget;
mc.img_mc0.y += mc.speed;
mc.img_mc1.y += mc.speed;
if (mc.flg_end){
checkEndPoint(mc);
}
if (mc.flg_stop){
checkStopPoint(mc);
}
ckeckFrameOut(mc);
if (mc.flg_remove){
mc.lg_remove = false;
//停止時の位置ずれ補正
mc.img_mc0.y = mc.pos_stop;
if (mc.img_mc0.y < 0){
mc.img_mc1.y = mc.img_mc0.y + mc.img_mc0.height;
} else {
mc.img_mc1.y = mc.img_mc0.y - mc.img_mc0.height;
}
if (mc.img_mc1.y == 0){
mc.img_mc0.y = mc.img_mc1.y - mc.img_mc0.height;
}
mc.removeEventListener("tick",spin);
stopSlot(mc);
}
}
//停止監視
function checkStopPoint(mc){
if (mc.pos_stop == mc.img_mc0.y){
mc.flg_stop = false;
mc.flg_remove = true;
} else {
var margin = Math.abs(Math.abs(mc.pos_stop) - Math.abs(mc.img_mc0.y));
if (margin < mc.speed){
mc.speed = margin;
}
}
}
//
function onEndTimer_spin(mc){
//終了フラグ:on
mc.flg_end = true;
}
//停止地点チェック
function checkEndPoint(mc){
var i;
var dis_mim = mc.img_mc0.height;
//var posNo = 0;
var flg_check = false;
//---スロットをランダムにする
for (i=0;i<array_pos.length;i++){
if (mc.img_mc0.y >= 0 && mc.img_mc0.y <= array_pos[i]){
flg_check = true;
break;
} else if (mc.img_mc0.y < 0 && mc.img_mc0.y <= array_pos[i]*-1){
flg_check = true;
break;
} else {
flg_check = false;
}
}
if (flg_check){
var dis = Math.abs(array_pos[i] - Math.abs(mc.img_mc0.y));
if (dis < dis_mim){
dis_mim = dis;
posNo = i;
}
}
var fugo = (mc.img_mc0.y < 0) ? -1 :1;
mc.pos_stop = array_pos[posNo] * fugo;
//絵柄番号:判定(img_mc0.yが+値の時は、img_mc1の位置が当たりになる)
imgNo = (mc.pos_stop < 0) ? posNo :(array_pos.length-1) - posNo;
imgNo = (imgNo == 4) ? 0:imgNo;
mc.flg_end = false;
var amari = dis_mim % mc.speed;
if (amari > 0){
mc.img_mc0.y += amari;
}
mc.flg_stop = true;
}
function stopSlot(mc){
//startボタン:使用可
useStartBtn(mc.parent);
}
function useStartBtn(target){
target.btn_start.addEventListener("mouseover", onOver_start);
target.btn_start.addEventListener("mouseout", onOut_start);
target.btn_start.addEventListener("click",onClick_start);
target.btn_start.cursor = "pointer";
target.btn_start.alpha = 1;
}
function nouseStartBtn(target){
target.btn_start.removeEventListener("mouseover", onOver_start);
target.btn_start.removeEventListener("mouseout", onOut_start);
target.btn_start.removeEventListener("click",onClick_start);
target.btn_start.cursor = "normal";
target.btn_start.alpha = 0.3;
}
AS版ではスロット(slot_mc0)MovieClipの中に書かれていたスクリプトも、Canvas版ではルートのタイムラインにまとめてあります。MovieClipの中にスクリプトを書かない方がいいと聞いたのでと、JSだとやはり1ファイルにまとまっていたほうがわかりやすいので。
※その為Canvas版では、【slot_mc0のタイムライン】の冒頭(1〜10行目)で書かれている変数も、固定値のもの(1〜2行目)はルートに書き、その他(3〜10行目)はスロット(slot_mc0)のプロパティにしてあります。また関数には引数でMovieClipを渡すようにしてあります。
で、ここからが本題のCanvas版用にJavaScriptで書き換える時の注意点。
1.オブジェクトの基準点が違う
まず気をつけないといけないのは、Cavnasに描き出されたにMovieClipの座標は、Flash CC上での座標(+)ではなくて中心点(○)であるということ。これを知らないで座標を取得すると訳のわからない位置が表示されてハマります。なので必ずオブジェクトの座標と中心点が重なるようにしておくこと。
【参考】
・Chitose Web: Toolkit for CreateJS 座標がずれる時
2.オブジェクトのwidth,heightが取れない。
そしてもう1つ。DisplayObject.getBounds()という一見オブジェクトのサイズを返してくれそうなメソッドがありますが、これが役に立たないということ。そもそも作られた目的が違うようです。詳しくは下記を参照。
【参考】
・FN1312005 | EaselJS 0.7.1: ColorFilterクラスでインスタンスの塗りのグラデーションを反転させる | HTML5 : テクニカルノート(FumioNonaka.com)
※「03 Shapeオブジェクトに矩形領域を定める ー DisplayObject.getBounds()とDisplayObject.setBound」を参照
またDisplayObjectにwidth,heigitのプロパティがありません。スロット(slot_mc0)をループさせるために中のMovieClipの高さが必要なので、ここではimg_mc0とimg_mc1のy座標の差を高さとして算出し(【Canvas版JavaScript】19〜20行目)、img_mc0とimg_mc1に「height」というプロパティ名で渡しています(【Canvas版JavaScript】4行目)。
※ img_mc0の高さはわかっているので固定値で変数にすることも可能ですが、絵柄を増やして高さが変更になることも考慮して計算で出しています。
3.Timerの使い方
AS版ではスロット(slot_mc0)の回転をEvent.ENTER_FRAMEでやって、Timerを一定間隔で動かして回転速度と停止のコントロールをしていたのですが、EaselJSで該当するTickerは静的クラスでインスタンス化できないので複数作れません。
・EaselJS v0.8.0 API Documentation : Ticker
なので、回転開始時間をcreatejs.Ticker.getTime()で取得してスロットのプロパティに設定し(【Canvas版JavaScript】23行目)、経過時間をチェックして擬似的にインターバル処理を行っています(onCheckTimer_spin/【Canvas版JavaScript】87〜107行目)。
【参考】
・JavaScript – CreateJS(EaselJS)でタイマを管理するTickerクラスの使い方 – Qiita
※JSのTimerを使えばいいのでは?という指摘を受けましたが、書いてる時点ではCreateJSの中のメソッドしか頭になく気がつきませんでした(笑)。そちらの方が、TimerとENTER_FRAMEにそれぞれ対応していていいかもしれません。
4.thisの参照
スタートボタンの制御を関数化していますが(useStartBtn/nouseStartBtn)、ここでも問題が。AS版のようにボタンを「this.btn_start」で参照すると、オブジェクトがないよと怒られます。
[Error] TypeError: undefined is not an object (evaluating 'this.btn_start.addEventListener')
console.log(“this >> “+this)で調べてみると、ルートで参照した場合は・・・
this >> [MovieClip (name=null)]
となり、functionの中で参照した場合は・・・
this >> [object Window]
と返ってきます。
FlashCCが書き出すHTMLを見てみてると・・・
<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.test02_html5();
stage = new createjs.Stage(canvas);
stage.addChild(exportRoot);
stage.update();
createjs.Ticker.setFPS(lib.properties.fps);
createjs.Ticker.addEventListener("tick", stage);
}
</script>
となっていて、Flashからの要素をexportRootにラップしてstageにaddChildしているので、ルートで参照したときのthisの値([MovieClip (name=null)])は、exportRootを指しています。
一方、functionの中で呼び出したものはページのルートであるWindowを指すようです。
ここではカンタンにルートから呼び出す関数にはthisを渡して参照するようにしました(【Canvas版JavaScript】10行目、218〜225行目)。
※他の方法としては、直接exportRootを参照に使うか、stageの中から参照することも可能です。
exportRoot.btn_start.addEventListener("mouseover", onOver_start);
stage.getChildAt(0).btn_start.addEventListener("mouseover", onOver_start);
またはJSのFunction.bind()を使って、thisの参照を書き換えた変数を生成して呼び出す方法もあります。
var func = useStartBtn.bind(this.btn_start);
func(this);
【参考】
・FN1303001 | CreateJSで関数にスコープを定める ー proxy()関数 | HTML5 : テクニカルノート
・FN1303002 – 関数に任意のthis参照を定める – HTML5 : テクニカルノート
・JavaScript の超便利なメソッド bind で this を制御する | Foreignkey, Inc.
5.まとめ
処理 |
AS |
JavaScript |
オブジェクトの座標(x,y) |
座標(+) |
中心点(○) |
オブジェクトのサイズ(width,height) |
width,height |
プロパティなし |
ルートのthis参照 |
root |
exportRoot |
functionのthis参照 |
root |
Window |
繰り返し処理 |
Event.ENTER_FRAME |
Ticker |
タイマー |
Timer |
Timer(setTimeout/setInterval) |
上記の説明は、私の環境のFlash CCで書き出される下記のバージョンの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="test01_html5.js"></script>