前回の記事では具体的なコードがあまり無かったので、今回はその辺りをもう少し詳しく。
準備
サンプルとして、適当なオブジェクトを作ってそこにメソッドその他をぶら下げていく事にします。後の処理で汎用的に使う関数もここで用意しておきます。
var NBNOTE = {}; /* □ オブジェクトの型を判定 --------------------------- */ NBNOTE.is = function( type, obj ) { var c = Object.prototype.toString.call( obj ).slice( 8, -1 ); return obj !== undefined && obj !== null && c === type; };
CSSプロパティ実装の判定
プロパティ名から実装を判定、対応していればベンダープレフィックスを必要に応じて付与して返します。メモ化したりクロージャ使ったりで頑張ってみましたが、まだまだどうにかできそう…。
NBNOTE.cssProp = (function() { var cache = { element: document.createElement( 'div' ), vendor: (/webkit/i).test( navigator.appVersion ) ? ['webkit', '-webkit-'] : (/firefox/i).test( navigator.userAgent ) ? ['Moz', '-moz-'] : (/msie/i).test( navigator.userAgent ) ? ['ms', '-ms-'] : 'opera' in window ? ['O', '-o-'] : '', transitionEvent: { webkit: 'webkitTransitionEnd', Moz: 'transitionend', ms: 'MSTransitionEnd', O: 'otransitionend' } }; function cssProp( name, camelcase ) { var index = typeof camelcase === 'undefined' ? 0 : camelcase ? 0 : 1, element = cache.element, is3d = name === 'transform-3d', prop; if ( cache.hasOwnProperty( name ) ) { return cache[name][index]; } if ( name.toLowerCase() === 'transitionend' ) { if ( typeof cssProp( 'transition' ) === 'undefined' ) { return undefined; } return cache.transitionEvent[cache.vendor[0]]; } name = is3d ? 'transform' : name; prop = name.replace( /-./g, function( m ) { return m.charAt( 1 ).toUpperCase(); } ); if ( prop in element.style ) { if ( !is3d ) { cache[name] = [prop, name]; return cache[name][index]; } else { element.style[prop] = ''; element.style[prop] = 'rotateY(100deg)'; if ( element.style[prop] === '' ) { cache['transform-3d'] = [undefined, undefined]; } else { cache['transform-3d'] = [prop, name]; } return cache['transform-3d'][index]; } } prop = cache.vendor[0] + prop.replace( /./, function( str ) { return str.toUpperCase(); } ); if ( prop in element.style ) { if ( !is3d ) { cache[name] = [prop, cache.vendor[1] + name]; return cache[name][index]; } else { element.style[prop] = ''; element.style[prop] = 'rotateY(100deg)'; if ( element.style[prop] === '' ) { cache['transform-3d'] = [undefined, undefined]; } else { cache['transform-3d'] = [prop, cache.vendor[1] + name]; } return cache['transform-3d'][index]; } } cache[name] = [undefined, undefined]; return undefined; } return cssProp; })();
以下は上で作った NBNOTE.cssProp() のラッパー。引数にとったプロパティ名から、実装を判定して真偽値を返します。配列を使えば、複数のプロパティ全てに対応するかどうかの判定結果を得る事もできます。
NBNOTE.checkCSSSupport = function( props ) { if ( NBNOTE.is( 'String', props ) ) { return typeof NBNOTE.cssProp( props ) !== 'undefined'; } var i = props.length; for ( ; i--; ) { if ( typeof NBNOTE.cssProp( props[i] ) === 'undefined' ) { return false; } } return true; };
それぞれの使い方は以下。Opera 12の場合の例になります。
// transform に対応している NBNOTE.cssProp( 'transform' ); // 'oTransform' // ...が、3D変形には対応していない NBNOTE.cssProp( 'transform-3d' ); // undefined // キャメル記法でなくハイフン区切りの文字列で取得 NBNOTE.cssProp( 'transform', false ); // '-o-transform' // transitionEnd イベントを待つ $('#hoge').on( NBNOTE.cssProp( 'transitionEnd' ), function(){ console.log( 'finished.' ); } ) // transition に対応している? NBNOTE.checkCSSSuport( 'transition' ); // true // transition と transform(3D変形) の両方に対応している? NBNOTE.checkCSSSuport( [ 'transition', 'transform-3d' ] ); // false
トランジションの適用
前回の記事では、以下の例みたいにトランジションを適用しやすいようjQueryを拡張しておけば良いかも、と書いていました。
~ // 実装の判定 ~ // CSSトランジションを適用 $( elm ).nbAnimate({ transform: 'rotateX(90deg)' }, 1000, 'ease-in', function(){ console.log( 'transition finished' ); } }); ~
しかしこの例だと、複数のトランジションや処理を順番に実行する場合、コールバックのネストがどんどん深くなっていってしまう問題があることに気が付きました(遅い)。
// 順番に実行したい時にコールバックのネストが深くなってしまう... $( elm ).nbAnimate({ transform: 'rotateX(90deg)' }, 1000, 'ease-in', function(){ $( elm ).nbAnimate({ transform: 'rotateX(180deg)' }, 1000, 'ease-in', function(){ $( elm ).nbAnimate({ transform: 'rotateX(270deg)' }, 1000, 'ease-in', function(){ alert( 'finished.' ); } ); } ); } });
という訳でもっと良さげな実装を考えていたところ、Flash用の偉大なフレームワーク「Progression」にある SerialListクラスを思い出しました。これなら上記の問題も解決できるし扱いやすい!ということで、今回の用途に合う形で試しに落とし込んでみました。
/* □ シリアルリスト - Progression同名クラスの見よう見まね --------------------------- */ NBNOTE.SerialList = function() { this.now = 0; this.state = 0; this.list = []; }; NBNOTE.SerialList.prototype = { addCommand: function() { var len = arguments.length; if ( !len ) return; for ( var i = 0; i < len; i++ ) { this.list[this.list.length] = arguments[i]; } }, execute: function() { var that = this; if ( that.state ) return; that.state = 1; var cnt = 0, c = that.list[that.now], d = function() { that.now++; that.state = 0; if ( that.now < that.list.length ) { that.excute(); } else { that.now = 0; that.list = []; } }; if ( NBNOTE.is( 'Array', c ) ) { for ( var i = 0; i < c.length; i++ ) { c[i].done = function() { cnt++; if ( cnt === c.length ) { d(); } }; c[i].run(); } } else { c.done = d; c.run(); } } }; /* □ 遅延コマンド --------------------------- */ NBNOTE.Wait = function( time ) { this.time = time; }; NBNOTE.Wait.prototype = { run: function() { var that = this; setTimeout( function() { that.done(); }, that.time ); } }; /* □ 関数コマンド --------------------------- */ NBNOTE.Func = function( func ) { this.func = func; }; NBNOTE.Func.prototype = { run: function() { this.func(); this.done(); } }; /* □ CSS Transformsコマンド --------------------------- */ NBNOTE.Transform = function( target, value, duration, easing ) { this.target = target instanceof jQuery ? target : $( target ); this.value = value; this.duration = duration ? duration : 1000; this.easing = easing ? easing : 'linear'; }; NBNOTE.Transform.prototype = { run: function() { var that = this, cnt = 0; that.target .on( NBNOTE.cssProp( 'transitionEnd' ), function() { cnt++; if ( cnt === that.target.length ) { that.target .off( NBNOTE.cssProp( 'transitionEnd' ) ) .css( NBNOTE.cssProp( 'transition' ), '' ); that.done(); } } ); setTimeout( function() { that.target .css( NBNOTE.cssProp( 'transition' ), 'all ' + that.duration + 'ms ' + that.easing ) .css( NBNOTE.cssProp( 'transform' ), that.value ); }, 25 ); } }; /* □ jQuery.css()コマンド --------------------------- */ NBNOTE.CSS = function( target, value ) { this.target = target instanceof jQuery ? target : $( target ); this.value = value; }; NBNOTE.CSS.prototype = { run: function() { this.target.css( this.value ); this.done(); } }; /* □ jQuery.animate()コマンド --------------------------- */ NBNOTE.Animate = function( target, obj, duration, easing ) { this.target = target instanceof jQuery ? target : $( target ); this.obj = obj; this.duration = duration ? duration : 'normal'; this.easing = easing ? easing : 'linear'; }; NBNOTE.Animate.prototype = { run: function() { var that = this; that.target .animate( that.obj, that.duration, that.easing ) .promise() .done( function() { that.done(); } ); } };
色々と足りてない部分は後から付け足していくとして…とりあえずコマンド用のクラス(?)は、遅延(Wait)、関数呼び出し(Func)、トランジション(Transform)、jQuery.css()(CSS)、jQuery.animate()(Animate)の5つを用意。メソッドはコマンド追加の addCommand() と 実行の execute() のみ。書き方はSerialListクラスとほぼ同じで、以下のような感じ。
var target = $( '#hoge' ); var serialList = new NBNOTE.SerialList(); // コマンドを追加 serialList.addCommand( // トランジション new NBNOTE.Transform( target, 'rotate(360deg)', 500, 'ease-out' ), // 1秒待つ new Wait( 1000 ), // 配列の中身は同時に実行される(はずだけど挙動が怪しいので要調査...) [ // トランジション new NBNOTE.Transform( target, 'rotate(0deg)', 500, 'ease-out' ), // jQuery.animate() new NBNOTE.Animate( target, { top: 20 }, 500, 'linear' ) ], // 関数呼び出し new Func( function(){ alert( 'finished.' ); } ) ); // 実行 serialList.execute();
うーん、もうちょいちゃんと作らないとダメかな…(‘A’;)
最後に、実際に動かしてみたサンプルへのリンクを以下に置いておきます。IEで見ると(最新バージョンでも transition に対応していないので)アラートが出ます。IE以外のモダンなブラウザで見ると多分ちゃんと動きます。動いたらいいな。