/*
* Updated 2008/11/09
*
* Mail : yunos@graviness.com
* Home : https://www.graviness.com/
*/
/**
* アニメーションクラス(抽象クラス).
* {@link #init()}, {@link #move()}, {@link #destroy()}メソッドをサブクラスでオーバーライドする必要があります.
* サブクラスのコンストラクタで必ずこのクラスのコンストラクタを呼び出して下さい.
*
* ■ インスタンス変数
* private int time 描画間隔
* private Timer timer タイマー.
* private int currentFrame 現在のフレーム位置.
* private int finalFrame 最終フレーム.
* コンストラクタで初期化後,この値を変更した場合,動作は保証されません.
*
* private Function[] initializeListeners アニメーション初期化時処理関数群.
* {@link #init()}メソッドはインスタンス共通の処理になりますが,
* この関数群はインスタンス固有です.
* private Function[] destroyListeners アニメーション終了時処理関数群.
* {@link #destroy()}メソッドはインスタンス共通の処理になりますが,
* この関数群はインスタンス固有です.
*
* protected AnimationManager manager このアニメーションの管理オブジェクト.
* protected int managerIndices 管理オブジェクトで使用される
* このインスタンスのID群.
* protected int managerCurrentIndex 管理オブジェクトで使用される
* managerIndicesの参照位置.
*
* ■ アニメーションの原理
* JavaScriptでアニメーションを実現するには,以下の要素が必要です.
* ・移動(またはアニメーション)のアルゴリズム(moveメソッド).
* ・何段階で移動させるか(finalFrame変数).
* ・次の1段階の描画までどの程度の時間待つか(time変数).
* moveメソッドによって,始点から終点までどのような軌道で移動するかが決まり,
* finalFrame変数によって,その始点から終点までの軌道を何段階で描画するかが決まり,
* time変数によって,その段階段階の描画時間間隔が決まる.
*
* さて,アニメーション実行中,今どのフレームを実行しているかを示すcurrentFrame変数があり,この変数は最初0から始まり,フレームが一つ進むごとに+1される.currentFrameがfinalFrameに到達したとき,アニメーションは終了する.この+1されるまでの時間間隔はtime変数で決定される.
*
* ここで,どのフレーム位置にいるかという新たな変数tを定義する.
* フレーム位置 : t = currentFrame / finalFrame
* currentFrameは0から始まり,finalFrameに到達したとき,アニメーションは終了するため,tの範囲は[0.0, 1.0]になる.
* 現在のフレーム位置を示すcurrentFrameの範囲は相対位置を示すのに対し,tは絶対位置であることが重要である.変数tを定義することによって,tは必ず[0.0, 1.0]の範囲をとるため,finalFrameに依存しない移動アルゴリズムを作成することができる.
*
* 以上のようなことを実際のプリミティブなプログラムに書くと以下のようになる.
* ---
* var currentFrame = 0; // 現在のフレーム位置
* var finalFrame = 32; // 最終フレーム位置
* var time = 20; // 再描画時間間隔 [msec]
*
* var e = document.getElementById("MyId");
*
* function run() {
* if (currentFrame ≦ finalFrame) {
* var t = currentFrame++ / finalFrame;
* e.style.left = move(t) + "px";
* setTimeout("run();", time);
* }
* // アニメーション終了処理
* else {
* ...
* }
* }
*
* // アニメーション移動アルゴリズム
* function move(t) {
* // tは0.0から1.0まで変化するため,この関数は0〜100を返す.
* return Math.round(100 * t);
* }
*
* // アニメーションの実行
* window.onload = function() {
* run();
* };
* ---
*
* ■ finalFrame変数とtime変数の意味
* finalFrame変数とtime変数は,アニメーションの滑らかさを決定する変数である.
* finalFrame変数が大きい値であるほど,time変数が小さな値であるほど,アニメーションは滑らかになる.
*
* TVや動画の場合,FPS(Frame Per Second:1秒間当たり何コマ表示するか)があるが,time変数はこのFPSの値を決定する値である.FPS値はtime変数を使うと以下のように書ける.time変数の単位は[msec]であることに注意.
* FPS = 1000 / time [fps]
*
* ■ アニメーション実行時間
* finalFrame変数とtime変数により,アニメーション開始からアニメーション終了までの大まかな時間を算出することができる.
* アニメーション実行時間 [msec] = time * finalFrame
* ただし,上記は実際の画面への描画が0秒で行われることを想定したものであり,実際の実行時間は移動アルゴリズムおよびディスプレイの描画性能に大きく左右される.移動アルゴリズムが複雑であるほど,ディスプレイの描画性能が低いほど,アニメーション実行時間は上記式よりも長くなる.よって,アニメーション実行時間を厳密に想定して,プログラムを書くことは好ましくない.例えば,あるアニメーション終了後に他のアクションを起こすなどの処理は,currentFrame,destoryListenerおよびdestoryメソッドによって制御されるべきである.
* @see graviness-anim-AnimationManager.js
*
* ■ initメソッドとinitializeListenerの関係
* initメソッド,initializeListenerはアニメーション開始時に実行されるアニメーション初期化時処理関数である.
* initメソッドはこのクラスから派生された具象クラスで定義される全インスタンス共通の初期化時処理であり,インスタンス個別に初期化時処理を行う場合,addInitializeListenerメソッドによって追加されたリスナを使用する.よって,initメソッドにインスタンスごとに変化するような処理を記述することは好ましくない.
* destroyメソッドとdestroyListenerの関係も同様である.
*/
function class__AbstractAnimation__(window) {
var className = Object.getClassName(arguments.callee);
var $time = Object.privateId();
var $timer = Object.privateId();
var $currentFrame = Object.privateId();
var $finalFrame = Object.privateId();
var $initializeListeners = Object.privateId();
var $destroyListeners = Object.privateId();
var $run = Object.privateId(); // METHOD
/**
* ※必ずサブクラスのコンストラクタで呼び出す必要がある.
*/
var F = window[className] = function() {
this[$time] = 16;
this[$timer] = window.setTimeout("", 0);
window.clearTimeout(this[$timer]);
this[$currentFrame] = 0;
this[$finalFrame] = -1;
this[$initializeListeners] = null;
this[$destroyListeners] = null;
this._manager = null;
this._managerIndices = null;
this._managerCurrentIndex = 0;
};
var FP = Object.getPrototype(arguments.callee, window);
/**
* 描画間隔を設定.単位はmsec.
*/
FP.setTime = function(time) {
this[$time] = time;
};
/**
* 描画間隔を取得.単位はmsec.
*/
FP.getTime = function() {
return this[$time];
};
/**
* 現在のフレーム位置を設定.
*/
FP.setCurrentFrame = function(currentFrame) {
this[$currentFrame] = currentFrame;
};
/**
* 現在のフレーム位置を取得.
*/
FP.getCurrentFrame = function() {
return this[$currentFrame];
};
/**
* 最終フレームを設定.
*/
FP.setFinalFrame = function(finalFrame) {
this[$finalFrame] = finalFrame;
};
/**
* 最終フレームの取得.
*/
FP.getFinalFrame = function() {
return this[$finalFrame];
};
/**
* アニメーション開始時処理.
* protected f()
* アニメーション開始時に呼ばれます.
* アニメーション開始に先行する処理を行う必要がある場合,サブクラスでオーバーライドします.
*/
FP.init = function() {
};
/**
* アニメーション終了時処理.
* protected f()
* アニメーション終了時に呼ばれます.
* このメソッドが呼び出されるタイミングは以下の2つです.
* ・アニメーション時,フレーム位置が最終フレームに到達したとき.
* ・{@link #stop()}メソッドが呼ばれたとき.
* ・ユーザが明示的にこのメソッドを呼び出したとき.
*
* アニメーション終了時に処理を行う必要がある場合,サブクラスでオーバーライドします.
* アニメーション終了時の処理であり,アニメーションを終了させることはできないことに注意して下さい.
* 終了させたいときは{@link #stop()}をコールします.
*/
FP.destroy = function() {
};
/**
* メインメソッド(抽象メソッド).
* public abstract f(int currentFrame, int finalFrame, double t)
* 実際のアニメーション(軌跡等)の具体的な処理を記述する.
*
* このメソッドが偽の値を返すと,即座にアニメーション終了処理に移行します.
*
* @param currentFrame 現在のフレーム位置.
* @param finalFrame 最終フレーム位置.
* @param t フレーム長を1.0としたときの現在のフレーム位置.
* 0.0 ≦ t ≦ 1.0.t = currentFrame / finalFrame.
* @return アニメーション終了時:false / アニメーション続行時:true.
* 偽を返すと即座にアニメーションは終了処理に移行します.
*/
FP.move = function(currentFrame, finalFrame, t) {
return void(window.errorMessage("See abstract-method " + className + "#move(int, int, double)"));
};
/**
* アニメーションを開始する.
* void f()
* アニメーション開始時に呼び出します.
*
* このメソッドは,以下の順番で処理を行います.
* 1. 現在のフレーム位置を0に設定.
* 2. {@link #init()}メソッドコール.
* 3. 初期化時リスナの実行.
* 4. アニメーション開始.
*
* 初期化時リスナの実行タイミングは,{@link #init()}メソッドコール後です.
*/
FP.start = function() {
// MEMO: ここで0を入れるのは機能性に欠けるが,単一マネージャへの複数登録が可能になったため必要性は上がった.
this[$currentFrame] = 0;
this.init();
var ls = this[$initializeListeners];
if (ls != null)
for (var i = 0; i < ls.length; i++)
if (ls[i] != null)
ls[i](this);
this[$run]();
};
/**
* アニメーション実行メソッド.
* private void f()
*/
FP[$run] = function() {
if (this._manager != null) {
var animations =
this._manager.getAnimations(this, this[$currentFrame]);
if (animations != null)
for (var i = 0; i < animations.length; i++)
animations[i].start();
}
if (this[$currentFrame] <= this[$finalFrame]
&& this.move(this[$currentFrame], this[$finalFrame], this[$currentFrame] / this[$finalFrame])) {
this[$currentFrame]++;
this[$timer] = window.setTimeout(function(o) {
return function() {o[$run]();};}(this), this[$time]);
} else {
this.stop();
if (this._manager != null) {
var animations = this._manager.getAnimations(this, -1);
if (animations != null)
for (var i = 0; i < animations.length; i++)
animations[i].start();
}
}
};
/**
* アニメーションを停止(または,スキップ)する.
* void f()
* このメソッドを呼び出すと即座にアニメーションは停止・終了します.
* 現在のフレーム位置が最終フレーム位置に達すると自然に停止・終了しますが,
* それ以外のタイミングで停止・終了する場合に呼び出します.
*
* このメソッドは,以下の順番で処理を行います.
* 1. 現在のフレーム位置を最終フレーム位置以上に設定.
* 2. タイマの停止(描画の停止).
* 3. {@link #destroy()}メソッドコール.
* 4. 終了化時リスナの実行.
*
* メソッド呼出し後はそのときのフレーム位置が保持されません.
*/
FP.stop = function() {
this[$currentFrame] = this[$finalFrame] + 1;
window.clearTimeout(this[$timer]);
this.destroy();
var ls = this[$destroyListeners];
if (ls != null)
for (var i = 0; i < ls.length; i++)
if (ls[i] != null)
ls[i](this);
};
/**
* アニメーション初期化時リスナを追加.
* void addInitializeListener(Function listener)
*
* {@link #start()}メソッド呼び出し時にここで追加した関数が実行される.
* {@link #init()}メソッドはインスタンス共通ですが,
* ここで追加される処理はインスタンス固有です.
*
* @param listener リスナ呼び出し時,リスナの第一引数に,このインスタンスが渡されます.よって,listenerはfunction (animationInstance) {...}の形式が望ましいです.
*/
FP.addInitializeListener = function(listener) {
if (listener === void 0
|| listener == null
|| typeof listener != "function")
return void(window.errorMessage("IllegalArgumentsException@"
+ className + "#addInitializeListener(Listener)"));
if (this[$initializeListeners] === null)
this[$initializeListeners] = new Array();
this[$initializeListeners].push(listener);
};
/**
* アニメーション初期化時リスナを削除.
* boolean f(Function listener)
* {@link #addInitializeListener(Function)}で追加したリスナを削除します.
* @return 正常に削除されたとき真.
*/
FP.removeInitializeListener = function(listener) {
if (listener === void 0
|| listener == null
|| typeof listener != "function")
return void(window.errorMessage("IllegalArgumentsException@"
+ className + "#removeInitializeListener(Listener)"));
if (this[$initializeListeners] === null)
return false;
var ls = this[$initializeListeners];
for (var i = 0; i < ls.length; i++) {
if (ls[i] === null)
continue;
if (ls[i] === listener) {
ls[i] = null;
return true;
}
}
return false;
};
/**
* アニメーション終了時リスナを追加.
* void addDestroyListener(Function listener)
*
* {@link #stop()}メソッド呼び出し時にここで追加した関数が実行される.
* {@link #destroy()}メソッドはインスタンス共通ですが,
* ここで追加される処理はインスタンス固有です.
*
* @param listener リスナ呼び出し時,リスナの第一引数に,このインスタンスが渡されます.よって,listenerはfunction (animationInstance) {...}の形式が望ましいです.
*/
FP.addDestroyListener = function(listener) {
if (listener === void 0
|| listener == null
|| typeof listener != "function")
return void(window.errorMessage("IllegalArgumentsException@"
+ className + "#addDestroyListener(Listener)"));
if (this[$destroyListeners] === null)
this[$destroyListeners] = new Array();
this[$destroyListeners].push(listener);
};
/**
* アニメーション終了時処理を削除.
* boolean f(Function listener)
* {@link #addDestroyListener(Function)}で追加したリスナを削除します.
* @return 正常に削除されたとき真.
*/
FP.removeDestroyListener = function(listener) {
if (listener === void 0
|| listener == null
|| typeof listener != "function")
return void(window.errorMessage("IllegalArgumentsException@"
+ className + "#removeDestroyListener(Listener)"));
if (this[$destroyListeners] === null)
return false;
var ls = this[$destroyListeners];
for (var i = 0; i < ls.length; i++) {
if (ls[i] === null)
continue;
if (ls[i] === listener) {
ls[i] = null;
return true;
}
}
return false;
};
} class__AbstractAnimation__(window);