07 | 2017/08 |  09

  1. 無料サーバー

User forum-FC2BLOG-Info-Edit Template-Post-Edit-Upload-LogOut

CSSやJavascript自習の苦闘史を綴っていきたい。恐縮ですがJavascriptを有効にしてご覧ください。
2005年12月から社会問題も掲載!


スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

jquery.js を利用した Ajax 通信の一例(改訂版コードについて)

コード改訂に至る経緯

約 2 年程前に上記関連リストに掲載したエントリイを投稿しました。

そこではjquery.js の Ajax コードを活用する一例として、今閲覧しているエントリイの直前 10 エントリイ、直後 10 エントリイ及び最新の 10 エントリイの、それぞれのタイトルを Ajax 通信で取得してみました。

その後ずっとそれを使い続けてきたのですが、あることからコードを見直す必要に迫られ、今回改めてコードを全面的に見直し、要所をいくつか改訂しました。

改訂を思い立ったのは、直前/最新/直後エントリイタイトルの表示位置が、ブラウザによっては、どうも思ったように描画されていないことに最近気がついたからです。(セットしてから 2 年も経っているのに今更かい?!(^^;; )

IE と Firefox では意図したとおりになっていたのですが、safari や chrome ではずれていたのです。

そしてその原因は、自作コードにおける offsetParent に対する曖昧な対処方法であろう、と推測できました。

しかし、これだけのことでは 2 年前のコードを改訂する動機として弱すぎました。2 年も経つと自作コードであっても可読性はかなり低下するので、思い出す時間と、当時の到達点まで改めて登る努力が必要となります。

重い腰を上げたのは、先月に解読した jQuery(~).offset メソッドや jQuery(~).position メソッドを、今後様々に活用したいと思ったからです。このブログ内で、これらのメソッドや Animation に関するメソッドを活用して、様々な実験を行いたいと思っているのですが、そのためには HTML 要素の位置が正確に把握できなければなりませんし、CSS の決まり事をしっかり押さえなければなりません。

こうして 「offsetParent の扱いを曖昧にしたままでは適切な実験が行えない」 と判断し、曖昧なコードを改善しなければならなくなったのです。

▲ToTop

今回の作業(コード改訂作業を含む)の苦労談(戒めのためのメモ)

1. offsetParent 問題

今年 1 月迄はこのブログの container 要素 には position 指定を行ってきませんでしたが、今回の一連の改訂作業を通じて container 要素には position:relative を指定しました。(※ このブログの container要素は body 要素直下にあり、ブログタイトルや時計、カレンダーそしてエントリイ等を包含する要素 )

その container の中に Javascript コードによって、10項目ずつの以前 / 以後 / 最新エントリイタイトルを表示する 3 つの要素( 以後「popup要素」と呼ぶ )を絶対配置するのですが、container の親要素及び祖先要素に position 指定をしていない状態では、W3C 勧告に従えば、popup 要素の offsetParent は body となります。

しかし、IE( ver 8 の標準モード以外の IE )では、祖先要素に明示的に width や height 値が指定されている要素があると、それが offsetParent となってしまいます。そしてこれまで position 指定をしていなかった container には width が指定されています。この結果 IE 7 以前で閲覧すると、popup 要素が container 内に意図したとおりに表示されていたのです。

他方、safari や chrome では、popup 要素の表示位置が意図した位置から「ずれて」いました。W3C 勧告に従っているこれらのブラウザでは、container 要素内に置かれた popup 要素の offsetParent が body となっていたために、思惑と異なってずれていたのです。(但し、同じように W3C に従っているはずの Firefox では、IE 同様に container 内に収まっていました。その理由を未解明です。)

さて、このようなブラウザによる「ぶれ」を放置したままでは、要素を自在に配置することは出来ません。当然画面内で要素を任意の位置に移動させるアニメーションも思うように操れません。

しかし、offsetParent や offsetTop、offsetLeftプロパティは、ブラウザによって解釈が異なる場合があるだけではなく、そもそも W3C 標準ですらありませんHTML要素の位置取得 - Rails で行こう!)。そのためそのまま使うだけでは、クロスブラウザ対応にはなりません。そこで利用したいと考えているメソッドが、jquery.js の offset と position インスタンスメソッドです。

ところで、W3C 勧告には至っていませんが、offsetParent などについては 「 CSSOM View Module 」という草案が存在しています。そして、jquery.js はそれに準拠するよう配慮されています。

そこで、Ajax コードを改定する前に、まず container 要素に position:relative を指定し、当該要素内に配置される要素の offsetParent が container 要素になるようにしました。これにより CSSOM View Module( W3C Working Draft 04 August 2009) に従って、要素を配置する前提条件が整うことになります。

こうしておいて、jquery.js の offset と position インスタンスメソッドを活用して、 CSSOM View Module に従って、どのブラウザでも同じ位置に要素を配置することを目指します。但し、この問題はエントリイを改めて述べます。

▲ToTop

2. Ajax 通信が終わったことを Javascript インタープリタに知らせる方法

案の定、Ajax 通信コードの改訂作業には時間が掛りました。

久しく Ajax コードを弄っていなかったため、コード内での Ajax 通信独特の「時間差」への配慮も、最初は全く忘れていました。また jqquery.js における Ajax 通信処理の理解が半可通であることも思い知らされました。そこで、自戒を込めて再学習した jquery.js の Ajax 通信コードの要点を整理しておくこととします。

2 年前に完成を見て、つい数日前まで使ってきた従前バージョンの get3modeEntryTitles.js では、setInterval タイマー関数と jQuery.active プロパティ( このプロパティは Ajax 通信の終了を記録する flag の役割を果たしている )を組み合わせて、Ajax 通信の終了を Javascript インタープリタに認識させていました。

しかし、今回改訂した最新の get3modeEntryTitles.js では、ajaxStop イベントハンドラーを使って同じ処理をさせることにしました。この方がコードがすっきりするし、そもそのそのような用途として開発された Ajax イベントハンドラー関数を利用しない手はないからです。

3. 先行する Ajax 通信による取得値を利用する後発 Ajax 通信の起動タイミングの設定

後発 Ajax 通信が先発 Ajax 通信結果を利用する場合、後発 Ajax 通信の開始タイミングがコードの正否を分けます。

勿論、単に後発通信コードを先発通信コードの後に記述するだけでは、何の解決にもなりません。先発通信結果を利用するのですから、それが終わらない限り後発通信を開始してはいけないのに、単にコード記述上前後の順番通りに書くだけでは後発通信は成功しません。

コード進行速度は Ajax 通信速度を遙かに上回るので、先行通信が終わらないうちに後発通信が開始されてしまい、後発通信は必要な情報を先発通信から取得できないからです。

そこで改訂版では、setInterval 関数を起動して、後発通信で使用する先発通信結果の変数が存在するまでループさせ、その存在を確認できてから後発通信を開始するようにしました。

改訂前は jQuery.active 値がゼロになるまでタイマーを繰り返し起動していたのですが、必要な変数の存在確認の方が直接的・具体的であり、可読性も高まります。

▲ToTop

改訂した get3modeEntryTitles.js コード

HTML 文や CSS 文は、jQuery Ajax を活用したエントリイタイトルの各種取得法──集大成! に掲載したものを大部分踏襲しました。但し、CSS 文において visibility 属性の使用はやめ、display属性で統一することにしました。今回 faseIn/Out メソッドによって popup 要素の表示/隠蔽を行うように変えたのですが、これは jquery.js の 各種 animation メソッドは、表示/隠蔽の切り替えに display 属性を利用しているためです。

■改訂した get3modeEntryTitles.js コード
/*
 * fileName : get3modeEntryTitles.js
 * released : 2008/03/01
 * verup : 2008/07/04, 2008/08/08, 2008/08/17, 2010/02/14, 2010/05/15
 */
  1:// 個別エントリイ表示モードでない場合には、何もしないでコード進行を終える。
  2:if (location.href.indexOf("entry")==-1) (function (){return;})();
  3:else{ //個別エントリイ表示の時
  4:(function($){
  5:  var now=function(){return +new Date;};  // 現在時刻取得
  6:  $.tr={ //所要時間計測・待機起動回数記録用 jQuery 拡張オブジェクト
  7:    start:now(),
  8:    registerEvent:"",
  9:    ajax:{ recent:{},before:{},after:{} },
 10:    end:"",
 11:    setEndingCnt:0,waitRecentAjaxCnt:0
 12:  };
 13:  var loading="<div id='loading' class='ta_c'><img src='http://blog-imgs-31.fc2.com/h/k/o/hkom/loading_16.gif' alt='' border='0' width='16' height='16' /> Now Loading...</div>";
 14:  $(function(){
 15:    $("#container").append(
 16:      "<div id='popup_before' class='popup'>" +loading+"</div>",
 17:      "<div id='popup_recent' class='popup'>"  +loading+"</div>",
 18:      "<div id='popup_after' class='popup'>"  +loading+"</div>"
 19:    );
 20:
 21:    // イベント発生要素に沿って popup 要素を配置する(表示へ別メソッドで行う))
 22:    var $popup = $(".popup"),$navi=$(".navi_container"),left,top;
 23:    // 各 popup の縦位置(ナビゲブロックの高さだけ下に)指定
 24:    top = $navi.position().top + (parseInt($navi.css("marginTop")) || 0)+ $navi.outerHeight();
 25:    $popup.width(600);
 26:    var space = $("#container").innerWidth()-$popup.outerWidth();
 27:    // 各 popup の横位置指定
 28:    $.each( ["before","recent","after"] ,function(i,item){
 29:        left= !i ? 0 : i===1 ? space/2: space;
 30:        $('#popup_'+item).css({left:left+"px", top:top+"px"});
 31:    });
 32:
 33:    // 前・最新・後リストの表示/隠蔽イベントハンドラー
 34:    $.each( ["before","recent","after"] ,function(i,item){
 35:      $("#"+ item + "EntryTitle" ).hover(
 36:        function(){  $('#popup_'+item).fadeIn(); },
 37:        function(e){
 38:          var itsIdName = e ? e.relatedTarget.id : window.event.toElement.id;
 39:          if (itsIdName.indexOf("popup") < 0)
 40:            $popup.fadeOut();
 41:        }
 42:      );
 43:    });
 44:
 45:    // 表示された popup の hover イベントハンドラー
 46:    $popup.hover(
 47:      function(e){if (!$(this).is(":hidden")) return;},
 48:      function(e){ $(this).fadeOut(); }
 49:    );
 50:  });  //End of $(function(){})
 51:
 52:  // variable
 53:  var html = {recent:[],before:[],after:[]}, getStr={recent:0,before:0,after:0},
 54:    lastNo, regExpr,thisEntryNo, complement,itval,border={before:0, after:0},realElm = {before:0,after:0},flagStopHandler = false;
 55:
 56:  // 最近のタイトルを取得する関数を定義
 57:  var makeRecentEntryList = function (limit){
 58:    var No, subject, date, iter=0, ret=[], $chd;
 59:    var target ={    //数字は xml ファイル内で登場する順番
 60:          link:[],    //0
 61:          title:[],    //1
 62:      //  description:[],  //2
 63:        //  content:[],    //3
 64:          subject:[],    //4
 65:          date:[]      //5
 66:      };
 67:    $.ajax({
 68:      url: /http:.+fc2\.com\//.exec(location.href)[0] + "?xml" || null,
 69:      type: "GET",
 70:      dataType: "xml",
 71:      global:false,
 72:      success: function(xml){
 73:        var tmpStr = '<div>'+decodeURI(encodeURI("最新のエントリイ情報がありません。"))+'</div>';
 74:        if (xml==undefined || xml==null) {getStr.recent = tmpStr; return;}
 75:        $.tr.ajax.recent["start"]=now();
 76:        $.each(target,function(key){
 77:          $.each($("item",xml), function(i,n){
 78:            $chd = $(n).children();
 79:            ret[i]= [$chd.eq(0), $chd.eq(1), $chd.eq(4),$chd.eq(5)];
 80:            target[key].push( ret[i][iter].text() );
 81:          });
 82:          iter++;
 83:        });
 84:        for (var i=0; i < limit; i++) {
 85:          No = /entry-([0-9]+)/.exec(target.link[i])[1];
 86:          i==0 && (lastNo = Number(No));
 87:          subject =" , " +target.subject[i];
 88:          date =" , " +target.date[i].substring(0,10);
 89:          html.recent.push( "<li><a href='" + target.link[i] + "' target='_blank'>" +  target.title[i] + "</a> (No." + No + subject + date + ")</li>");
 90:        }
 91:        $.tr.ajax.recent["end"]=now();
 92:        getStr.recent =  "<div class='ta_c'><em>Recent " + limit + " Entries</em></div><ul class='ml_1_5'>" + html.recent.join('') + "</ul>";
 93:      }  //End of success()
 94:    });  //End of ajax()
 95:  };  //End of makeRecentEntryTitle func
 96:
 97:  // 前後のタイトルを取得するための準備を行う
 98:  var makeEntryList = function(beforeafter,limit){
 99:    var thisHTTP, getEntryNos=[], thisURL=[];
100:    regExpr = /(http:.+entry-)([0-9]+)/;
101:    thisEntryNo = Number(regExpr.exec(location.href)[2]);
102:    thisHTTP = regExpr.exec(location.href)[1];
103:    border[beforeafter] = Math.min(limit+1, beforeafter == "before" ? thisEntryNo :(Number(lastNo)-thisEntryNo+1) );
104:    if (border[beforeafter]==1) thisURL.length=0;
105:    else {
106:      for (var i=1; i < border[beforeafter]; i++)
107:        thisURL.push(thisHTTP + (thisEntryNo - (beforeafter == "before" ? i : -i)) +".html" );
108:    }
109:    // 準備完了! Ajax 通信開始
110:    getTitlesByAjax.call(this, beforeafter,thisURL);
111:  };
112:
113:  // Ajax 通信により前後のエントリイタイトル等を取得する関数の定義
114:  function getTitlesByAjax(tense,thisURL){
115:    if ( thisURL.length != 0 ) {
116:    $.each(thisURL,function(i,aryitem){
117:      $.tr.ajax[tense][i]={};
118:      $.tr.ajax[tense][i]["start"]=now();
119:      $.get(aryitem,function(data){  //data は thisURL[i] の html テキスト文
120:        regExpr = /<title>(.*)<\/title>/;
121:        var titleStr = regExpr.exec(data)[1].substring(19);
122:        if ( /\S+/.test(titleStr)){  //空白だけのタイトル名は補足しない。
123:          ++realElm[tense];
124:          html[tense][i] = "<li><a href='" + aryitem + "' target='_blank'>" + titleStr +" (EntryNo." + /entry-([0-9]+)/.exec(aryitem)[1] + ")</a></li>";
125:        }
126:        $.tr.ajax[tense][i]["end"]=now();
127:      },"text");
128:    });
129:    }
130:  };
131:
132:  function ajaxStopHandler(){
133:    flagStopHandler = true;
134:    $.each(["before","after"],function(i,tense){
135:      if (realElm[tense]==0)
136:        getStr[tense]="<div class='ta_c'>"+ (tense=='before' ? "Before " : "After ") + "Entry "+ decodeURI(encodeURI('はありません。')) + "</div>";
137:      else {
138:        complement = (border[tense]-1 -realElm[tense]!==0) ? 
139:          " ( " +decodeURI(encodeURI('欠番があります。')) +" )" : "";
140:        getStr[tense] = "<div class='ta_c'><strong>"+ (tense==='before' ? "Before " : "After ") + realElm[tense] + " Entries" + complement +"</strong></div><ul style='margin-left:1.5em;list-style-type:disc'>" + html[tense].join('') + "</ul>";
141:      }
142:    });
143:    var beforeTime= realElm.before!==0 ? "<li>以前タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.before[realElm.before-1].end-$.tr.ajax.before[0].start)/1000 +" 秒</li>" : "";
144:    var afterTime= realElm.after!==0 ? "<li>以後タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.after[realElm.after-1].end-$.tr.ajax.after[0].start)/1000 +" 秒</li>" : "";
145:    var AjaxLog ="<ul style='margin:0.5em;border-top:white dotted 1px;padding:0.5em 0.5em 0 1em;list-style-type:circle'>"+
146:      "<li>クリック後 Ajax 通信開始迄の所要時間: "+($.tr.ajax.recent.start-$.tr.start)/1000 +" 秒</li>"+
147:      "<li>最新タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.recent.end-$.tr.ajax.recent.start)/1000 +" 秒</li>"+ beforeTime + afterTime +
148:      "<li>このプロジェクト全体の所要時間: "+ (($.tr.end=now())-$.tr.start)/1000 +" 秒</li></ul>"+
149:      "<div style='text-align:right;margin:-2em 1em 0.5em 0'><button style='display:block;margin:-1em 0 0 auto;width:16em' title='このボタンをクリックするとここで行った Ajax 通信に関するこのブログのエントリイが開きます。' onclick='this.blur();window.open(\"http://hkom.blog1.fc2.com/blog-entry-626.html\",target=\"_blank\")'>この Ajax 通信や所要時間について</button></div>";
150:    $(function(){
151:      $("#popup_before").html(getStr.before + AjaxLog);
152:      $("#popup_after").html(getStr.after + AjaxLog);
153:      $("#popup_recent").html(getStr.recent + AjaxLog);
154:    });
155:  }
156:
157:  $(document).ajaxStop(function(){
158:    ajaxStopHandler();
159:  });
160:  // 前後及び最新のタイトルリスト作成
161:  makeRecentEntryList(10);  //最近リスト取得実行
162:  makeEntryList("before",10);  // 以前エントリイ
163:  function nextAjaxTimer(){
164:    $.tr.waitRecentAjaxCnt++;
165:    // makeRecentEntryList により lastNo 値を取得するまでは起動しない。
166:    if (!!lastNo) {
167:      if (ival) {clearInterval(ival);ival=null;}
168:      makeEntryList("after",10); // 以後エントリイ
169:    }
170:  };
171:  var ival = setInterval(nextAjaxTimer,100);
172:})(jQuery);
173:}

Navigate Bar に係る Ajax Javascript コードを見直した。

Navigate Bar を改善した

ここで言うところの Navigate Bar とは、各エントリイ最上部付近に表示している

「 直前のEntry(4) | ホーム(5) | 直後のEntry(6) 」

のことである。

「改善した」と言ってもそのバーの見た目は全く変えていない。

このバーは、3 つの文字列の上にマウスカーソルを載せると、直前の 10 本の記事タイトル、最近の 10 本の記事タイトル、あるいは直後の 10 本の記事タイトルを、それぞれの文字列に対応して表示するためのものだ。そして、この表示に jquery.js の Ajax 関係コードを利用していることは改めて触れるまでもないだろう。

今回その表示 / 隠蔽方法を jquery.js で提供されている fadeIn / Out メソッドに変更したのであるが、変更はそれだけではない。

記事リストの表示位置は以前と全く変えていないが、その表示/隠蔽コードを全面的に見直し、エントリイ番号 750 ~ 754 において解読した offset、position、innerWidth/Height、outerWidth/Height メソッドを積極的に利用するように改訂したのだ。

これによりコードがすっきりしたことは言うまでもないが、一連の位置・サイズ関連メソッドの利用実績を重ねることに意義があった。

そして案の定、実際に利用してみて貴重な成果を得ることが出来た。

変更結果

こちらのエントリイをご覧ください。「jquery.js を利用した Ajax 通信の一例(改訂版コードについて) 2010/02/15(No.762)

IE における jQuery("A").css("marginTop") 返値の特異性

或る HTML 要素 A ( この要素は position:absolute ; に指定されていると仮定する)の margin-top 値を、スタイルシートにおいて margin-tpo:0; に指定したと仮定する。すると、誰しもその場合には jQuery("A").css("marginTop") は ゼロを返すと予想する。実際 Firefox ではきちんとゼロを返す。

ところが IE8(標準モードで描画した)では、何と auto が返される。これは明らかなバグであるが、このことによって、Navigate Bar に関するコードのエラー原因がなかなか掴めず、エラー箇所の特定にこの土日を費やしてしまったのである。

またしても IE 対策に時間が割かれ、恨めしい思いだけが残ったのだ。

IE よ。Microsoftよ。いい加減にしてくれ!!!!──と絶叫したい。バグを知らなければ満足に利用できない代物は、ユーザーフレンドリィではないし、いくら無料の製品とはいえ、欠陥品と言えるのではないだろうか?!!

jquery.js が 1.4 にアップデート

それは恒例行事となった

確か 1 月 14 日が jquery.js の誕生日なのだそうだ。

そして、毎年、その時が来ると確実にバージョンアップが重ねられてきた。

今年も同様に、1.3.2 から 1.4 へとバージョンアップされ、非圧縮ファイルでは 何と 6000 行を数えるまでに巨大化した。

さて、現在進行中の「 HTML 要素の位置・サイズ取得に関わるメソッド等の解読 」作業に当たって、1.3.2 でほぼ作業を終えていたのだが、折角バージョンアップされたのだからと、大変なルーティンワークだったが、「 HTML 要素の位置・サイズ取得に関わるメソッド等の解読 」に係るエントリイ(エントリイNo.749 ~753)を、全て 最新版の 1.4 対応に改訂した。

その改訂作業を行いながら気になったのだが、今回のバージョンアップでは、かなり堅実な記法が採用されていることだ。if 条件 を適用する行が 1 行しかなくても(必要ないのに) { } で括ったり、余り必要ないと思われる箇所でも this[0] をわざわざ elem 変数に代入したり、不要と思われる過剰な記述が目立つのだ。

これはおそらく、バグチェックしやすいように、確実な記法を採用した結果と思われるが、クールな記法が特徴だった jquery.js だけに、何故か寂しい気がする。

ますます巨大化する jquery

しかも、jquery.js は年々巨大化している。致し方ないこととは言え、今後も止まることなく巨大化を続けていくのだろうか?

機能の豊富さをキャッチアップしていくこと自体、大変な手間であり、そのことが利用のハードルを高くするのではないか、と懸念する。

例えば、簡易版のようなものを検討する必要があるのではなかろうか、とさえ思えてくる。まあ、そのようなものとして使える core.js があるが...

豊富な機能に圧倒されつつ...

それでも食らいついて、キャッチアップしようと必死になっているのは、jquery.js が javascript 学習の「教科書的」存在と言えるからである。

その存在を知り、大胆不敵にも解読しようと決意したのは、2007 年 10 月だった。

そして、その存在を知るまでと、2 年余の学習を重ねてきた現在とを比べれば、Javascript に関する知識とその利用度合いは、幼稚園児と大学生程の天地の差が付いている。我ながら嬉しい驚きを覚えるほどだ。

そのような進化を遂げることが出来たのは、偏に jquery.js に食らいき、それを何とか理解してやろう、そして理解した上で jquery.js を利用して縦横無尽にコードを書いてみたい、という単純であるが、強い欲求の結果に他ならない。

今後もますます巨大化し、多機能化することは間違いないだろうが、引き続き食らいついて行こう、と決意を新たにしている昨今である。

jquery.js (1.4) による要素位置の測定と適正な配置 (5) コード解読 (4)

jQuery().innerHeight()/innerWidth()/outerHeight()/outerWidth() メソッド、並びに jQuery().height()/widdth() メソッドの概要

これらの 6 つのインスタンスメソッドは、HTML 要素の大きさを計測するか、あるいは大きさを指定するメソッドです。(サイズ指定は、height 及び width メソッドのみ。つまり content だけがサイズ指定できる。)

まず、以上のメソッドを分かりやすく図解してみました。横幅の説明だけですが、敢えて煩雑にしてまで縦方向を示す必要はないでしょう。

これらのメソッドが、W3C 勧告の視覚整形モデルに準拠したボックスの各部寸法を取得するためのものであることは、図から一目瞭然です。

各メソッドのサイズ測定箇所図

jQuery().innerHeight()/innerWidth()
HTML 要素の padding 辺間のサイズを計測する読み取り専用メソッドで、返値は全て単位なしのピクセル数となります。
jQuery().outerHeight(margin)/outerWidth(margin)
引数 margin が true の場合には、HTML 要素の margin 辺間のサイズを計測する読み取り専用メソッドとなり、引数がないか false の場合には、HTML 要素の border 辺間のサイズを計測する読み取り専用メソッドとなります。ここに、返値は全て単位なしのピクセル数となります。
jQuery().height(size)、jQuery().width(size)
引数がある場合には、HTML 要素の content の高さや幅を指定し、そのサイズで表示させます。その際の指定単位は px に限定されず、適正な文字列ならば em %など他の単位も使用できます。
他方、引数がない場合には、HTML 要素の content の高さや幅の、単位なしのピクセル数を取得します。

jQuery().innerHeight()/innerWidth()/outerHeight()/outerWidth() メソッド、並びに jQuery().height()/widdth() メソッドのコード解読

これらのメソッドは 1 つの jQuery.each クラスメソッド内で定義されています。

このクラスメソッド内では、三項演算子が頻繁かつ多重的に使用されており、全体で 50 行近い長いコードとなっています。

これらのメソッドは html 要素に適用できるので、ページ内の各要素のサイズ計測に利用できることは勿論ですが、window、body、document などの頁等の全体を指す要素に対して適用して、文書全体の高さや window の横幅などの情報を得ることも出来ます。

5946:// Create innerHeight, innerWidth, outerHeight and outerWidth methods
   // 'Height' と 'Width' を対象として巡回走査する
5947:jQuery.each([ "Height", "Width" ], function(i, name){
5948: // i が 1 の時には変数 tl に Leftを、i が 0 の時には Top を代入する。
5949: var type = name.toLowerCase();
5950:
5951: // innerHeight and innerWidth
    // jQuery インスタンスメソッド innerHeight 及び innerWidth を定義する。
5952: jQuery.fn["inner" + name] = function(){
5953:  return this[0] ? // インスタンスに HTML 要素が登録されていれば
      // 最初の HTML 要素の padding 辺までの height 又は width プロパティ値を取得する。
      // false 指定があるので算出スタイル値ではなく指定済みの CSS 値が取得される。
5954:   jQuery.css( this[0], lower, false, "padding" ) :
5955:   null; // インスタンスに HTML 要素が登録されていなければ null 値を返す。
5956: };
5957:
5958: // outerHeight and outerWidth
    // jQuery インスタンスメソッド outerHeight 及び outerWidth を定義する。
5959: jQuery.fn["outer" + name] = function(margin) {
5960:  return this[0] ? // インスタンスに HTML 要素が登録されていれば
      /* 引数が true ならばその margin 辺までの、引数が
       * 無指定か false ならばその border 辺までの、height 又は width CSS
       * スタイル値を取得する。ここでも false 指定があるので、算出スタイル値
       * ではなく指定されている CSS スタイル値が取得される。
       */
5961:   jQuery.css( this[0], lower, false, margin ? "margin" : "border" ) :
5962:   null; // インスタンスに HTML 要素が登録されていなければ null 値を返す。
5963: };
5964: 
    // jQuery インスタンスメソッド width() 及び height() を引数 size 付きで
    // jQuery.prototype オブジェクトに登録する。
5965: jQuery.fn[ type ] = function( size ) {
5966:  // Get window width or height
5967:  var elem = this[0]; // インスタンスに登録されている最初の html 要素への参照
5968:  if ( !elem ) { // elem がない場合の処理である
      // 引数 size がなければ nullを返し、あれば jQuery インスタンスを返す。
5969:   return size == null ? null : this;
5970:  }
5971:  // <ケース 1> elem が window の時には、
5972:  return ("scrollTo" in elem && elem.document) ?
      // does it walk and quack like a window ?
      // ↑ は慣用句なのでしょうか?意味が分かりません。
5973:   // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
      // 人は皆は、標準/互換モード判別に document.documentElement か document.bodyを使用する。
      // CSS1標準モードならば document.documentElement.clientHeight/clientWidth を
5974:   elem.document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] ||
      // 互換モードならば document.body.clientHeight/clientWidth を返す。
5975:   document.body[ "client" + name ] :
5976:
5977:   // Get document width or height
      // elem が document の場合
5978:   (elem.nodeType === 9) ? // is it a document
5979:    // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
       // scrollWidth/Height 値と offsetWidth/Height 値の大きい方を返す。
5980:    Math.max(
5981:     elem.document.documentElement["client" + name],
5982:     elem.body["scroll" + name], elem.document.documentElement["scroll" + name],
5983:     elem.body["offset" + name], elem.document.documentElement["offset" + name]
5984:    ) :
5985:
5986:    // Get or set width or height on the element
       // <ケース 2> elem が window でない場合
5987:    size === undefined ? // <ケース 2-a>引数 size が未定義の時には
5988:     // Get width or height on the element
        // elem の height 値 または width 値を px 単位付きで返す。
5989:     jQuery.css( elem, type ) :
5980:
5991:     // Set the width or height on the element (default to pixels if value is unitless)
        /* <ケース 2-b>引数 size がある場合には
         * インスタンスに登録されている HTML 要素の height 又は width 値を
         * 引数が文字列型ならば size とし、引数が文字列型でないならば
         * size に px 文字列を追加した値とする。
         */
5992:     this.css( type, typeof size === "string" ? size : size + "px" );
5993: }; // 5965行の function はここまで
5994:
5995:});

jquery.js (1.4) による要素位置の測定と適正な配置 (4) コード解読 (3)

jQuery().scrollLeft(val)/.scrollTop(val) インスタンスメソッドの解読

これらのメソッドは引数を 1 つ取ることが出来、引数 val がある場合には指定した値だけ、window または対象要素を横または縦にスクロールさせます。

引数がなければ window または対象要素の、横または縦のスクロール量(ピクセル値、単位はなし)を取得します。

返値は val がある場合には jQuery インスタンスオブジェクトであり、val がない場合には発生しているスクロールのピクセル数です。

ここに window または対象要素にスクロールバーがない場合、引数 val にゼロでない値を指定しても当然対象要素にスクロールは発生せず、インスタンスオブジェクトが返されます。一方、引数 val がない場合にはゼロが返されます。

なお、このメソッド定義では jQuery.each クラスメソッド内に、jQuery(xxx).each インスタンスメソッドが入れ子になっており、かつ this が多用されています。その意味では、いかにも jquery.js ならではの、興味深いコードとなっています。

■jQuery().scrollTop()/scrollLeft() メソッドの解読
5901:// Create scrollLeft and scrollTop methods
   // 'Left' と 'Top' を対象として巡回走査
5902:jQuery.each( ['Left', 'Top'], function(i, name) {
5903: var method = 'scroll' + name; // 変数 method に 'scrollTop'、'scrollLeft'を順に代入
5904: // jQuery.prototype オブジェクトに scrollTop と scrollLeft メソッドを追加する
5905: jQuery.fn[ method ] = function(val) {
5906:  var elem = this[0], win; // ローカル変数定義
5907:  // jQuery インスタンスに登録された HTML 要素がなければ何もしないで終わる
5908:  if ( !elem ) {
5909:	  return null;
5910:  }
5911:
5912:  if ( val !== undefined ) { // <ケース1> 何らかの val が与えられている場合
5913:   // Set the scroll offset スクロール値をセットする
      // 'scrollTop' と 'scrollLeft'を巡回処理する。なお、この行の this は
      // elem をプロパティとして持つ jQuery インスタンスを参照する。
5914:   return this.each(function() {
5915:    win = getWindow( this ); // 関数起動
5916:
       // <ケース1-1> 登録要素が window ならば
5917:    if ( win ) {
5918:     win.scrollTo( // window.scrollTo メソッドを起動する
         /*  win.scrollTo(val,jQuery(win).scrollTop())、
          * 又は win.scrollTo(jQuery(win).scrollLeft(),val)
          * を実行する。
          */
5919:      !i ? val : jQuery(win).scrollLeft(),
5920:       i ? val : jQuery(win).scrollTop()
5921:     }):
5922:
5923:    } else { // 登録要素が登録要素が window ではない場合
5924:     this[mehod] = val; // 対象要素内でスクロールする。
5925:    }
5926:   });
5927:  } else { // <ケース2> val が未定義の場合
5928:   win = getWindow( elem );
5929:
5930:   // Return the scroll offset
      /* <ケース2-1> 登録要素が window の場合
       * win に pageXOffset プロパティが存在する場合には、
       * i が 1 ならば win.pageYoffset を、i が 0 ならば win.pageXoffset を返す。
       */
5931:   return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
        // jQuery.boxModel が true ならば document.documentElement[ method ] 
        // プロパティ値を取得する。(これは IE の標準モードの場合に該当する)
5932:     jQuery.boxModel && document.documentElement[ method ] ||
        // さもなければ document.body[ method ] プロパティ値を取得する。
        //(これは IE の互換モードの場合に該当する)
5933:     win.document.body[ method ] :
       // <ケース2-2> 登録要素が window でないならば、
       // 登録要素のスクロール値を取得して返す。
5934:    elem[ method ];
5835:  }
5836: };
5837:});
5938:
5939:function getWindow( elem ) { // 関数定義
     // elem に scrooTo メソッドが適用でき、かつ elem に document がある場合
     // つまり、elem が window の場合
5940:  return ("scrollTo" in elem && elem.document) ?
5941:    elem : // elem を返す。
5942:    elem.nodeType === 9 ? // そうでない場合には elem が html 要素ならば
         // elem の defaulView プロパティ値、又は elemの parentWindow プロパティ値を返す。
5943:      elem.defaultView || elem.parentWindow :
5944:      false; // elem が html要素でなければ false を返す。
5945:}

jquery.js (1.4) による要素位置の測定と適正な配置 (3) コード解読 (2)

jQuery().position() インスタンスメソッドの解読

offset メソッド と position メソッドの比較
各メソッドのサイズ測定箇所図

これら 2 つの jQuery インスタンスメソッドは、共に読み取り専用で、返値は共に座標オブジェクトですが、座標系と計測箇所が異なります。

offset メソッドがドキュメント座標における対象要素のボーダー辺の座標値を取得するのに対して、position メソッドは、対象要素の margin 辺の、基準要素( offsetParent )内における座標値を取得します。つま、offset メソッドによる座標値は対象要素の margin 値を含むのに対して、position メソッドによる座標値は margin 値を含みません。

しかし、position メソッドは offset メソッドを利用して基準要素から margin 辺までの offset 座標値を算出します。

jQuery().position() コード解読
5857:jQuery.fn.extend({ // jQuery protptype オブジェクトに position メソッドを
5858: position: function() { // インスタンスメソッドとして登録する
5859:  if ( !this[0] ) {// インスタンスに要素ノードが 1 つも登録されていなければ
5860:   return null;
5861:  } 
5862;
5863:   var elem = this[0]; // ローカル変数定義開始
5864:
5865:   // Get *real* offsetParent
      // 独自メソッドを使って真の offsetParent 要素を取得して、
      // offsetParent 変数に代入する
5866:   var offsetParent = this.offsetParent(),
5867:
5868:   // Get correct offsets
      // offset ローカル変数に計測対象要素の offset メソッドの返値を代入
5869:   offset = this.offset(),
      /* offsetParent が body または html ならば、ローカル変数 parentOffset に
       * { top: 0, left: 0 }オブジェクトへの参照を代入し、offsetParent が別の要素
       * ならば offsetParent.offset() の返値オブジェクトへの参照を代入する。
       */
5870:   parentOffset = /^body|html$/i.test(offsetParent[0].tagName) 
       ? { top: 0, left: 0 } : offsetParent.offset();
5871:
5872:   // Subtract element margins マージン値を減算する
5873:   // note: when an element has margin: auto the offsetLeft and marginLeft
5874:   // are the same in Safari causing offset.left to incorrectly be 0
      // 注記:「要素に margin : auto が設定されている場合、
      // safari では、offset.left が不正にゼロとなってしまうために、
      // offsetLeft 値 と marginLeft 値が同一になってしまう。」
 /* num メソッドは jquery.js の無名関数のトップレベルで定義されている関数
 * (#1266~1268)。第 1 引数で指定した HTML 要素の、第 2 引数で指定した算出 CSS
 * スタイル値(単位は含まない)を取得するもの。
 * 描画された状態から marginTop/Left 値を取得して、これを offset.top/left 値から
 * 減算する。offset() メソッドは border 辺までの値を保持しているので、この減算に
 * よって offset.top/left は margin 辺までの値となる。
 */
5875:   offset.top  -= num( this, 'marginTop'  );
5876:   offset.left -= num( this, 'marginLeft' );
5877:
5878:   // Add offsetParent borders
      // 上のコメントの通り、offsetParent の座標値に border 幅を加算する。
5879:   parentOffset.top  += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth",  true) ) || 0;
5880:   parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0;
5881:
5882:   // Subtract the two offsets
      // 2 つの offset 値を減算して、基準要素( offsetParent ) のパディング辺から
      // 対象要素の margin 辺までの距離を算出する。
5883:   return {
5884:    top:  offset.top  - parentOffset.top,
5885:    left: offset.left - parentOffset.left
5886:   };
5887:  },

jQuery().position() メソッド解説図

jQuery().offsetParent() メソッドの解読

#5865 にあるとおり、このメソッドは「Get *real* offsetParent」のために作成されました。

■jQuery().offsetParent() メソッド解読
5889: offsetParent: function() {
     // map メソッドの返値を返す。
5890:  return this.map(function() {
5891:   var offsetParent = this.offsetParent || document.body;
      // offsetParent が存在して、タグ名が body でも html でもなく、
      // position 値が static である限り繰り返す。
5892:   while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') ){
       // 1 つ上位の offsetParent への参照を変数 offsetParent に代入する。
5893:    offsetParent = offsetParent.offsetParent;
5894:   }
5895:   return offsetParent; // offsetParent 要素への参照を返す。
5896:  });
4296: }
4297:});

jquery.js (1.4) による要素位置の測定と適正な配置 (2) コード解読(1)

jquery.js ( ver1.4 ) における要素の位置指定等に関わるコード解読

解読対象コードは以下のメソッド等とし、この順に解読を行います。なお対象とする jquery.js はバージョンアップされたばかりの ver 1.4 とします。

jQuery.offset オブジェクト(5785~5854行)の概要

このオブジェクトには 3 つのメソッドが定義されます。

initialize は、ブラウザ特性を把握するために幾つかのプロパティを設定するメソッドです。

この「幾つかのプロパティ」とは次に列挙した 5 つで、これらは、jQuery().offset() メソッド解読において後述するように、要素の正確な位置算出を行うために使われ、それは同時に、各ブラウザの CSS に関する特性を捉えることになります。

  1. jQuery.offset.doesNotAddBorder : 或るボックスの offsetTop を算出する際に、直上の親ボックスのボーダー値を加算しないか(true)、加算するか(false)
  2. jQuery.offset.doesAddBorderForTableAndCells : table 内の或るセルの offsetTop 値を算出する際に、table ボックスのボーダー値を加算するか(true)、否か(false)
  3. jQuery.offset.supportsFixedPosition :或る要素を強制的に top:20px の位置に fixed 配置して、その offsetTop 値が 20px か 15px ならば true、その場の場合には false。 これは safari が offsetTop をマイナス 5 してしまうバグ対応のようだ。( 5px は offsetParent の top ボーダー幅である。)
  4. jQuery.offset.subtractsBorderForOverflowNotVisible : 或るボックス要素 A の offsetParent 要素の overflow 値を hidden にし、その position をrelative に指定した際に、A.offsetTop 値を A.offsetParent のボーダー高さだけマイナスするか(true)、しないか(false)
  5. jQuery.offset.doesNotIncludeMarginInBodyOffset : document.body に marginTop 値を設定した時に、body.offsetTop 値にその marginTop 値を加算しないか(true)、加算するか(false)

上の 5 つのプロパティは、ブラウザのそれぞれの仕様を個別具体的に把握するために用意されたもので、jQuery.offset オブジェクトの 1 つの目的は、この 5 プロパティチェックにあると言えるでしょう。これらのプロパティによってブラウザ毎のバグや仕様の差異がチェックされ、位置算出で補正が必要かどうか判断されるのです。

jQuery.offset オブジェクト内で定義される 2 つめのメソッド jQuery.offset.bodyOffset は、document.body の margin 値を top/left 値に追加し、その値をオブジェクトとして返します。返値の top/left 値は document.body のボーダー辺のドキュメント座標値を示すはずです。しかし、Firefox では body 部に border がある場合に、奇妙な offsetTop/Left 値を返すので、正確な座標値にならない場合があります。(このことの詳細は後述します。)

jQuery.offset オブジェクト内で定義される 3 つめのメソッド jQuery.offset.setOffset は、jquery.js ver1.4 で新設されたものです。

これまでの jQuery(elems).offset インスタンスメソッドは読み取り専用でしたが、ver 1.4 では、elems HTML要素(静的配置の場合も含む)を、配置換えする引数を取ることができるようになりました。この引数は配置換え先となる top、left プロパティを持つオブジェクト(仮に obj と呼ぶ)か、elems のインデックス番号 i と、i 毎の obj の 2 つの引数をもつ関数か、そのいずれかとなります。

jQuery.offset オブジェクトの 5 つのプロパティについて

前のエントリイで触れた W3C Working Draft 04 August 2009 - CSSOM View Module における offsetTop/Left プロパティ定義案を標準と考えた場合、jQuery.offset オブジェクトの 5 つのプロパティが、ブラウザ(全て windows 版)毎にどのようになるのかを一覧にしてみました。

ここに、doesNotSubtractBorderInBodyOffset は私が独自に作成したプロパティです。document.body にゼロではない border 幅が設定されている場合に、document.body.offsetTop 値から、このボーダー値が減じられてしまうかどうかを調べるために設けたものです。

上の表の説明
  1. W3C の列は W3C Working Draft 04 August 2009 - CSSOM View Module での提唱案に従った場合の各プロパティ値です。これを標準と考えて各ブラウザを比較してみます。

  2. すると、標準モードで border なしの場合、Opera と IE8 では doesNotAddBorder が false となることにおいて、Chrome では doesAddBorderForTableAndCells が false となることにおいて、最後に Firefox では doesNotSubtractBorderInBodyOffset が false となることにおいて、それぞれ標準とは異なっています。
    つまり、W3C 草案に全て一致するブラウザは現在存在しません。

  3. supportsFixedPosition は、position:fixed をサポートしない IE 及び Opera の後方互換モードでは false となります。

  4. Firefox では document.body に ゼロ幅ではない border がある時とない場合とで、offsetTop/Left が異なる値を示します。border があると offsetTop/Left 値は「マイナスボーダー幅」となるのです。標準でも互換でもいずれのモードでも同様です。

    そこで、doesNotIncludeMarginInBodyOffset プロパティを作って調査しました。

    こうして、Firefox だけが border 有無によって offsetTop に異なった値を返すことが判明しましたが、これは明らかにバグというべきでしょう。

  5. IE8 の場合、doesNotAddBorder は標準モードと互換モードで異なる値になります。これは IE 互換モードの content.width/height が W3C 標準と異なって border 辺までの値として定義されていることに関連していると推測されます。

subtractsBorderForOverflowNotVisible プロパティの目的

W3C CSS2.1 の仕様として、position : absolute と position : relative では配置原点が異なります。前者は offsetParent のパディング辺が基準となり、後者は offsetParent の内容辺が基準になります。そして内包されるボックスの margin 辺がそれぞれの配置原点に配置されます。(下図参照)

subtractsBorderForOverflowNotVisible プロパティは、或る要素 B の absolute 属性を一時的に relative に変更すると共に、B の overflow 属性を hidden にして、包含要素 C の padding 値だけ当該要素を右下に移動させた時に、当該要素内に静的に配置された子要素 A も B と一緒に動くかどうかを調べています。

上述の「 jQuery.offset オブジェクトの 5 つのプロパティ 」 に掲載した表にあるように、Windows 系の主要ブラウザにおいては、標準モードであれ互換モードであれ、subtractsBorderForOverflowNotVisible プロパティは全て false となります。ですから、どんなブラウザで、このプロパティ値が true となるのか分かりません。つまりこのプロパティの必要性が判然としません。

因みに、その他の jQuery.offset オブジェクトのプロパティでは、私が独自に設定した doesNotSubtractBorderInBodyOffset も含めて、いずれかのブラウザの、いずれかのモードにおいて、他と異なる値となるので、これらのプロパティはブラウザ特性を把握するために必要であることが分かるのですが、subtractsBorderForOverflowNotVisible プロパティだけは、必要性が分かりません。

このプロパティは、Mac 系のブラウザや Windows 系の他のブラウザにおいて、必要なのかもしれませんが、チェックし得ないため不明です。

jQuery.offset オブジェクトコード解読

■jQuery.offset オブジェクト
   // jQuery.offset オブジェクトの定義を開始する
5785:jQuery.offset = {
    // initialize メソッド定義を開始する。引数はない。
5786: initialize: function() {
     // ローカル変数定義
     /* 変数 html にはブラウザ特性をチェックするための 2 つの要素をセットしている。
      * 1 つは 4 辺共黒で塗りつぶした 5 px 幅のボーダーを持ち、コンテンツ幅も
      * 高さも 1 px の div タグ内に設けるチェック対象の空の div タグ。
      * そして 4 辺共黒で塗りつぶした 5 px 幅のボーダーを持ち、幅と高さを 1 px、
      * セル余白もセル間隔もゼロの table タグ内に配置するチェック対象の空 td タグ。
      * これらの 2 つの要素を document.body の左上隅にそれぞれ絶対配置して、
      * offsetTop プロパティを使ってブラウザ特性を測定する。
      */
5787:  var body = document.body, container = document.createElement('div'), innerDiv,
      checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0,
5788:   html = '
     
'; 5789:      // ローカル変数 container のインラインスタイルを設定する。 5790:  jQuery.extend( container.style, { position: "absolute", top: 0, left: 0,       margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } ); 5791: 5792:  container.innerHTML = html; // container 要素内に html 要素を挿入する      // その container を body 要素最前部に配置する 5793:  body.insertBefore(container, body.firstChild);      // ローカル変数への代入。innerDiv は checkDiv 要素の parentNode となる。 5794:  innerDiv = container.firstChild; 5795:  checkDiv = innerDiv.firstChild; 5796:  td = innerDiv.nextSibling.firstChild.firstChild; 5797:  // ブラウザ特性判定プロパティその 1── doesNotAddBorder を定義 5798:  this.doesNotAddBorder = (checkDiv.offsetTop !== 5);      // ブラウザ特性判定プロパティその 2── doesAddBorderForTableAndCells を定義 5799:  this.doesAddBorderForTableAndCells = (td.offsetTop === 5);      // #5800 ~ 5804 は ver 1.4 で追加された。 5800:  // checkDiv の位置指定を fixed とし、top 値を 20px とする。 5801:  checkDiv.style.position = "fixed", checkDiv.style.top = "20px"; 5802:  // safari subtracts parent border width here which is 5px      // safari では親要素のボーダー幅 5px を減算する。      // ブラウザ特性判定プロパティその 3── checkDiv.offsetTop が 20 あるいは      // 15 の時には true を supportsFixedPosition プロパティに代入し、      // そうでない場合には false を代入する。 5803:  this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);      // チェックを終えたので、chechDiv 要素の position と top プロパティの      // インラインスタイル指定を取り消す。 5804:  checkDiv.style.position = checkDiv.style.top = ""; 5805:  // checkDiv 要素の parentNode (これは offsetParent でもある)の      // overflow 属性を hidden とし、position 属性を相対配置とした上で 5806:  innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';      // ブラウザ特性判定プロパティその 4── subtractsBorderForOverflowNotVisible を定義 5807:  this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5); 5808:        // ブラウザ特性判定プロパティその 5── doesNotIncludeMarginInBodyOffset を定義 5809:  this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginToo); 5810:  // ブラウザ特性判定プロパティの設定を終えたので、container 毎 DOM Tree から削除する 5811:  body.removeChild(container); 5812:  this.initialized = true; // jQuery.offset.initialized プロパティを true に設定 5814: }, 5815: 5816: bodyOffset: function(body) { // bodyOffset メソッドの定義開始      // document.body 要素の offsetTop/Left 値をローカル変数に代入 5817:  var top = body.offsetTop, left = body.offsetLeft; 5818:  // jQuery.offset.initialize メソッドを起動する。 5819:  jQuery.offset.initialize(); 5820:  // doesNotIncludeMarginInBodyOffset が true ならば 5821:  if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {       // body の marginTop/Left の算出スタイル値を加算する。       なお、加算しないことは正しい offsetTop/Left の仕様です。       従ってここで margin 値を加算するのは、jQuery.offset.top/left プロパティ       によって、body 要素のボーダー辺の位置を得るために他なりません。 5822:   top += parseInt( jQuery.curCSS(body, 'marginTop', true), 10 ) || 0, 5823:   left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0; 5824:  } 5825: 5826:  return { top: top, left: left }; 5827: }, 5828: // ver 1.4 で新設されたメソッドで、offset 値を設定するメソッドである。 5829: setOffset: function( elem, options, i ) { 5830:  // set position first, in-case top/left are set even on static elem      // 設定対象とする elem 要素の position 算出スタイル値が static ならば 5831:  if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) { 5832:   elem.style.position = "relative"; // relative に変更する。 5833:  }      // elem の jQuery インスタンス jQuery(elem) への参照を変数 curElem に代入 5834:  var curElem = jQuery( elem ),       // curOffset に curElem の offset メソッド適用結果を代入 5835:   curOffset = curElem.offset(),       // elem 要素の top 算出スタイル値を curTop 変数に代入する。 5836:   curTop = parseInt( jQuery.curCSS( elem, "top", true ), 10 ) || 0,       // elem 要素の left 算出スタイル値を curLeft 変数に代入する。 5837:   curLeft = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0; 5838:  // 引数 options が関数ならば 5839:  if ( jQuery.isFunction( options ) ) {       // i と curOffset を引数として options 関数を起動し、その返値を options に代入する。 5840:   options = options.call( elem, i, curOffset ); 5841:  } 5842:      // offset.top/left の設定値から offset.top/left の現在値を減算し、      // それに top/left の現在値を加算する。要するに、差分を現在値に加算する。 5843:  var props = { 5844:   top: (options.top - curOffset.top) + curTop, 5845:   left: (options.left - curOffset.left) + curLeft 5846:  }; 5847:   5848:  if ( "using" in options ) { // options に using プロパティ(値は関数)があれば 5849:   options.using.call( elem, props ); // props を引数として using 関数を起動 5850:  } else { // using がなければ 5851:   curElem.css( props ); // 対象要素を props に従って配置する。 5852:  } 5853: } 5854:};

続いて、2 つめのコード解読を行います。

jQuery().offset() メソッドの概要

このメソッドの目的は、知りたい HTML 要素の border 辺のドキュメント座標値 top 及びleft を知ることです。そのために上述のように、jQuery.offset.bodyOffset オブジェクトの top/left プロパティに、body 部の margin 値を加算して border 辺迄の座標を得ています。(#4248~4255)

この結果、body 要素自体に border(幅 W )がある場合には、body.offsetTop = 0 (但し、firefox のみ、body.offsetTop = - W となってしまう)ですが、$("document.body").offset().top = marginTop 値 となります(ここでも firefox の場合のみ、marginTop - W となってしまいます)。

各メソッドのサイズ測定箇所図

このメソッドでは CSSOM で定義されようとしており、現時点では何ら標準化されていない 1 つのメソッド( getBoundingClientRect )を使って、あるいは同様に標準化されていない 3 つのプロパティ( offsetParent、offsetTop 及び offsetLeft )を使って、要素のドキュメント座標を算出します。

A.getBoundingClientRect メソッドをサポートしているブラウザにおいては、要素 A のクライアント座標値が取得でき、その値はブラウザ間で差異がないので、容易にドキュメント座標値が取得できます。A.getBoundingClientRect メソッドで取得したクライアント座標値に window スクロール値を加算し、clientTop/Left値を減算すれば、要素 A の正確なドキュメント座標値が取得できます。

一方、ブラウザが getBoundingClientRect メソッドをサポートしていない場合には、offsetParent、offsetTop 及び offsetLeft を利用してドキュメント座標を取得するのですが、これらのプロパティはブラウザ毎に解釈が異なっていたり、バグがあったりするので、そのままでは正しい位置が取得出来ません。そこで、上で解読した jQuery.offset オブジェクトの 5 つのプロパティを駆使して、ブラウザ毎・標準互換別の offsetTop プロパティ算出値の差異を補正し、jQuery.offsetParent メソッドによって、ブラウザ間で定義が異なる offsetParent の一意的な確定を行います。

こうして要素のドキュメント座標値( border 辺迄の値 )をブラウザに依存せずに正確に算出する訳です。

jQuery().offset() メソッドの欠点

jQuery().offset() メソッドには欠点があります。body 部に border がある場合、Firefox だけ正しい要素位置が算出できないのです。この場合に jquery.js コードの jQuery().offset() で取得される top と left 値は、共に border 幅の値だけ過小になってしまいます。

そこで、エントリイを改めてその補正を行うコードを jquery プラグインとして作成して掲載する予定です。

jQuery().offset() メソッドの詳細解読
■jQuery().offset() メソッドコード
   // html要素オブジェクトに getBoundingClientRect が定義されていれば、
   // つまりブラウザが getBoundingClientRect メソッドに対応していれば、
5690:if ( document.documentElement["getBoundingClientRect"] )
    // jQuery().offset() メソッドを prototype オブジェクトに組み込む
5691: jQuery.fn.offset = function( options ) {
     // インスタンスに登録されている最初の要素への参照を elem に登録する。
5692:  var elem = this[0];
5693:  // elem がないか、document がないならば
5694:  if ( !elem || !elem.ownerDocument ){
5695:       return null; // null を返す。
5696:  }
5697:
5698:  if ( options ) { // 引数 options があれば
5699:   return this.each(function( i ) { // 対象要素を巡回処理
        // setOffset クラスメソッドを呼び出す。
5700:     jQuery.offset.setOffset( this, options, i );
5701:   });
5702:  }
5703:  // 対象要素が body の場合
5704:  if ( elem === elem.ownerDocument.body ) {
      // body を引数に bodyOffset クラスメソッドを 起動する
5705:   return jQuery.offset.bodyOffset( elem );
5706:  }
5707:  // インスタンスに格納されている最初の要素が document.body ならば
     // jQuery.offset.bodyOffset(document.body) の返値を返す。
     // ローカル変数定義。
     /* インスタンスに格納されている最初の要素の getBoundingClientRect()
      * オブジェクトへの参照を box に 、 document への参照を doc に、
      * document.body への参照を body に 、html 要素への参照を docElem に、
      * html.clientTop 又は document.body.clientTop 又はゼロを clientTop に 
      * html.clientLeft 又は document.body.clientLeft 又はゼロを clientLeft に、
      * それぞれ代入する。
      */
5708:  var box  = elem.getBoundingClientRect(), doc = elem.ownerDocument,
      body = doc.body, docElem = doc.documentElement,
5709:   clientTop = docElem.clientTop || body.clientTop || 0, 
      clientLeft = docElem.clientLeft || body.clientLeft || 0,
      // ローカル変数定義の続き
      /* top として、計測対象要素の top 値 + スクロール値 - clientTop を
       * left として、計測対象要素の left 値 + スクロール値 - clientLeft を
       * 代入する。
       */
5710:   top  = box.top  + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
5711:   left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
      // 返値定義
5712:
5713:   return { top: top, left: left };
5714: };
5715:// ブラウザが getBoundingClientRect に対応していない場合
5716:} else {
    // #5717~5783 は #5691~5714 に同じである。
5717: jQuery.fn.offset = function() {
5718:  var elem = this[0];
5719:
5720:  if ( !elem || !elem.ownerDocument ){
5721:   return null;
5722:  }
5723:
5724:  if ( options ) {
5725:   return this.each(function( i ) {
5726:     jQuery.offset.setOffset( this, options, i );
5727:   });
5728:  }
5729:
5730:  if ( elem === elem.ownerDocument.body ) {
5731:   return jQuery.offset.bodyOffset( elem );
5732:  }
5733:  jQuery.offset.initialize();
5734:  // ローカル変数定義
     /* elem に インスタンスに登録されている最初の要素(対象要素)への参照を、
      * offsetParent に elem の offsetParent への参照を、
      * prevOffsetParent に elem への参照を、doc に document への参照を、
      * docElem に html 要素への参照を、body に document.body への参照を、
      * defaultView に document.defaultView への参照を、prevComputedStyle に
      * document.defaultView.getComputedStyle(elem, null) オブジェクトへの参照を、
      * top に対象要素の offsetTop 値を、left に対象要素の offsetLeft 値を
      * それぞれ代入する。
      */
5735:
5736:  var offsetParent = elem.offsetParent, prevOffsetParent = elem,
5737:   doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
5738:   body = doc.body, defaultView = doc.defaultView,
5739:   prevComputedStyle = defaultView ? defaultView.getComputedStyle(elem, null) 
      : elem.currentStyle
5740:   top = elem.offsetTop, left = elem.offsetLeft;
5741:
     // 対象要素の親要素を elem に代入し、elem が body で html でもない限り
     // 巡回走査する。
5742:  while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
5743:   if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
5744:    break;
5745:   }
5746:
      // computedStyle 変数に対象要素の算出スタイルオブジェクトへの参照を代入する。
5747:   computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
      // スクロール値が重複加算されるのでその都度減算する。
5748:   top -= elem.scrollTop;
5749:   left -= elem.scrollLeft;
5750:
      // 親要素が offsetParent ならば
5751:   if ( elem === offsetParent ) {
       // 対象要素の親要素の offsetTop 値と offsetLeft 値を加算する。
5752:    top += elem.offsetTop;
5753:    left += elem.offsetLeft;
5754:
       /* 対象要素が 対象要素が table でも td でも th でもなく、かつ
        * doesNotAddBorder が true の場合、または、対象要素が table か
        * td か th で、かつ doesAddBorderForTableAndCells が true ではない場合
        */
5755:    if ( jQuery.offset.doesNotAddBorder && 
       !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) ){
        // 対象要素の親要素の border 幅値を top と left にそれぞれ加算する。
5756:     top  += parseInt( computedStyle.borderTopWidth,  10) || 0;
5757:     left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
5758:    }
5759:
       // 対象要素の offsetParent の offsetParent を 変数 offsetParent に代入
5760:    prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
5761:   }
5762:
      // subtractsBorderForOverflowNotVisible が true で、overflow 属性値が
      // visible ではない場合には
5763:   if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ){
       // 対象要素の親要素の border 幅値を top と left にそれぞれ加算する。
5764:    top  += parseInt( computedStyle.borderTopWidth,  10) || 0;
5765:    left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
5766:   }
5767:   // 対象要素の親要素の算出スタイルオブジェクトを prevComputedStyle 変数に
      // バックアップしておく。
5768:   prevComputedStyle = computedStyle;
5769:  }
5770:  // 対象要素の親要素の position 属性値が relative または static ならば、
5771:  if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ){
      // 最上位の offsetParent である document.body の offsetTop/Left 値を加算する。
5772:   top  += body.offsetTop;
5773:   left += body.offsetLeft;
5774:  }
5775:
     // ブラウザが position : fixed に対応しており、
     // かつ、対象要素の親要素の position 属性値が fixed ならば
5776:  if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ){
      // html.scroll 値か body.scroll 値の大きい方を top/left 値に加算する。
5777:   top  += Math.max(docElem.scrollTop, body.scrollTop);
5778:   left += Math.max(docElem.scrollLeft, body.scrollLeft);
5779:  }
5780:
5781:  return { top: top, left: left };
5782: };
5783:}

jquery.js による要素位置の測定と適正な配置 (1) はじめに

jQuery インスタンスメソッドである jQuery().offset や jQuery().position の解読を行おうと思い立って、jquery.js コードを睨み続けた結果、改めて offsetTop、offsetLeft について検討を加える必要があると思われました。

偶々、W3C による CSSOM View Module Working Draft が 2009年8月4日に公表されていたこともそうする必要性を高めました。 ( CSSOM View Module 作業草案 )、

たとえ「作業草案」とはいえ、拠って立つことのできる offsetTop/Left プロパティの定義として受け止めることの出来る内容となっています。こうして、この定義によって各ブラウザの差異を検証出来ることも、offsetTop/Left について再考するきっかけとなりました。

CSSOM View Module 作業草案 2009/8/4(以下「CSSOM VM草案」) における offsetParent、offsetTop/Left の定義

※ 以下は意訳であり、レイアウトボックスに関する名称は下図に拠り一部日本語化しました。

A.offsetParent の返値
1. 次の場合には null を返す。
  • A の中に CSS layout box が全くない場合
  • A がルート要素(HTMLタグ)の場合
  • A が body 要素の場合
  • A の position 算出CSSスタイル値が fixed の場合
2. A が map 要素内の area 要素の場合には、直近の祖先 map 要素を返す。
3. 次の場合には A の直近祖先要素を返す。
  • 直近祖先要素の position 属性が static ではない時
  • 直近祖先要素が body 要素の時
  • A の position 算出CSSスタイル値が static で、かつ祖先要素が td, th または table の時
A.offsetTop の返値
  1. A が body 要素の場合か、A に CSS layout box がない場合には、ゼロ
  2. A の offsetParent が null か body の場合には A の top border 辺の y 座標値
  3. 初期包含ブロックの原点を基準(つまり document 座標)として、A の border 辺の y 座標値から、offsetParent の padding 辺の y 座標値を減じた値
A.offsetLeft の返値
  1. A が body 要素の場合か CSS layout box がない場合には、ゼロ
  2. A の offsetParent が null か body の場合には A の left border 辺の x 座標値
  3. 初期包含ブロックの原点を基準(つまり document 座標)として、A の border 辺の x 座標値から、offsetParent の padding 辺の x 座標値を減じた値

CSSOM VM草案 における offsetTop/Left 定義の要点

上の CSSOM VM 草案の定義で押さえておかねばならないことは、第一に document.body.offsetTop/Left の返値を常にゼロと定義したことです。つまり、body 要素に margin、padding あるいは border があろうがなかろうが、それらとは無関係にゼロとしたことが重要な点です。

別のエントリイ(offsetLeft,offsetTop,offsetWidth そして offsetHeight──静的配置要素の絶対位置を確実に取得する方法について)で詳細に述べたように、FireFoxは、その最新版(Ver 3.5.7)においても body 要素に border が存在する場合には、document.body.offsetTop/Left は何とボーダー幅のマイナス値を返してしまいます。これは CSSOM VM 草案を待たずとも、奇妙な仕様と言わざるを得ません。

因みに、Windows 系の他のブラウザ(IE8、Opera10.10、Chrome 3.0.195.38、safari4.0.4)の標準モード及び後方互換モードでは、Firefox のような奇妙な演算は起こらず、たとえ body 部に border が設定されていたとしても、document.body.offsetTop/Left は全てゼロを返します。Firefox だけが「マイナス border-width」 という奇妙な値を返します。

第二に、確認を含めて押さえておくべきことは、分かりやすく記述すれば A.offsetTop/Left の値は、A.offsetParent 要素のパディング辺と A.offsetTop/Left が起動された A 要素の border 辺との間のピクセル数になる、と定義されていることです。

この点では msdn の エレメントの大きさと位置を測定する に掲載されている図は、明らかに間違っています。(その図では、A.offsetParent のパディング辺から A のパディング辺までの距離を A.offsetTop/Left としています。これは 10 年以上前から掲載されている図ですが、当時の IE 5 あたりの、今日呼ぶところの互換モード仕様に基づくものと解釈することも出来ない、誤った図になっています。)

以上の 2 点は、offsetTop/Left を理解し、使いこなす上で極めて重要な基礎的なことであり、コード作成上 offsetTop/Left を使用して要素位置を知り、その結果を踏まえて何らかの要素を配置する場合には、ブラウザ固有の「偏差」(バイオス)によってずれが生じないように留意する必要があります。(ここでは敢えて「バグ」と言う表現は使わないことにします。何故ならば、CSSOM View Module はまだ作業草案段階であって、勧告に至っていないからです。)

HTML文書内の要素位置取得方法の要点

はじめに

要素の正確な位置を知りたい場合( 絶対配置、相対配置又は静的配置のいずれの場合においても )、あるいはそれを踏まえて適正な位置に任意の要素を配置しようとする場合、ブラウザ毎に個別に対応するのは大変な手間となります。そこで様々な W3C 勧告が作られ、それを基準としてブラウザが作られてきました。しかし、未だに各ブラウザごとに固有のバグがあるため(特に IE では多数のバグがある)、バグ対策が不可欠となります。

また、IE で初めて採用され、その後各ブラウザが採用した、offsetParent や offsetTop/Left プロパティは、要素の位置を知るために非常に有効な読み取り専用プロパティですが、現時点では標準的仕様さえ存在しないため、利用に当たっては適正な個別対策が必要となります。

jquery.js における要素位置の把握方法の概要

jquery.js では、要素のドキュメント上の絶対座標を取得するメソッドとして jQuery().offset() を、また基準要素( つまり offsetParent 要素 )からの相対座標を取得するメソッドとして jQuery().position() を、それぞれ用意しています。また、これらのメソッド内で jQuery.offset オブジェクトの各プロパティが利用されます。(jquery.js ver 1.3.2 #4172~4197)

以下にこれらのコード解読を行いますが、その前に、jquery.js の位置算出方法の特徴に触れておきたいと思います。注目すべきことは 2 つあります。

第一に、getBoundingClientRect() メソッドという、余り見かけないメソッドを使うことによって、位置取得コードを非常に簡単にしていることです。このメソッドは(おそらく)全てのブラウザで同一の値が得られるので、クロスブラウザ対策が不要なのだと思います。

第二に、getBoundingClientRect() メソッドに対応していないブラウザへの対策として、良く行われているようなブラウザ判別法を採用していないことです。

よく見かけるコードでは、ブラウザ毎や描画モード毎に条件分岐させて、それ毎のプロパティ値を取得します。しかし、jquery.js はそのような伝統的手法を採用しません。

まず、位置算出対象とする HTML 文内に、Javascript コードによって visibility:hidden; position:absolute 状態の複数の要素( 要素 A 及びその offsetParent となる B 要素など )を挿入します。これによって、ブラウザ毎に位置算出値が異なる可能性を持った要素を作ります。

次に、A や B の offsetTop や offsetLeft 値を取得し、それが正しい値とならない場合にのみ、ブラウザによって描画され適用されたスタイル値( 算出スタイル値 )を取得して、正しい値を求めるのです。

このように CSS 算出スタイル値、つまり実際に描画された状態における CSS 値を利用することで、ブラウザのバグや固有のプロパティ仕様を「あるがままに受け入れ」、結果としての要素位置を算出している訳です。

なお、Windows 系の主要ブラウザ( IE、Firefox、Opera、safari、Chrome )では、標準モードでも互換モードでも、全て getBoundingClientRect() メソッドをサポートしているので、offsetParent/Top/Left プロパティを利用する位置算出コードは利用されないことになります。

もう一つ、コード解読を行う前に、 IE の名誉のために是非とも触れておかねばならないことがあります。

上に述べた jquery.js で定義された メソッドやオブジェクトにおいて、要素位置取得用に利用されるメソッドとプロパティ ── getBoundingClientRect()、offsetParent、offsetTop、offsetLeft ── は、全て IE が最初に実装し、他のブラウザも追随して採用した、という事実です。

Netscape Navigater が初めて採用し、しかし他のどのブラウザも採用しなかった layer オブジェクトとは対照的に、当時の IE4 や IE5 が意欲的に定義し、採用した所謂 DHTML モデルが、今日主流となっている W3C 勧告の DOM や CSS に至る起爆剤となったことは、過去において、IE が勝手な仕様を作り続けてきたことが混乱を招来してきたとしても、記憶に留めておくべき重要な事実でしょう。

jquery.js のアニメーションコードの活用 ( 4 ) slideToggleEx プラグインの簡便な活用のために(完全版)

slideToggleEx プラグインは不完全だった

エントリイNo.731 において、slideToggleEx 自作プラグインメソッドについて詳述しました。

ところが、そこで紹介したプラグインメソッドコードは不完全でした。隠蔽状態から起動するアニメーション対応を全く考慮していなかったので、その状態で起動すると思わぬ動きをしてしまうのです。つまり、実は半分しか完成していないのでした。(^^;; ヒヤアセ

そのことに気がついたのは 9 月中旬。その時点から、改めてコード全体を見直さざるを得なくなり、改めて animate メソッドの再学習を迫られたのです。

その後一ヶ月余り。四苦八苦、七転び八起きを繰り返した末に、昨晩やっと隠蔽状態からの起動にも対応した slideToggleEx 版が完成したので、ここに投稿することにします。

用意するもの・ことは以下の 5 つです。(この部分は No.731 の再掲)
  1. include 用 slideToggleEx プラグインファイルと include タグ要素
  2. slideToggleEx が適用されるタグ要素
  3. 起終点を右側にする場合には、対象タグ要素に width スタイル属性を設定します。また起終点を下にする場合には height スタイル属性を設定します。そうしないと animate メソッドの中で適正な幅や高さが計測されない場合があるためです。
  4. slideToggleEx を呼び出し、適用させるタグ要素(一般的には button タグでしょう)
  5. その button タグのクラス属性に fireSlideToggleEx-n を追加します。( n は 0 から 8 の整数)

n は起終点を意味し次の通りの意味を持ちます。

0:ボックスの中心、1:左上端、2:右上端、3:右下端、4:左下端、
5:上辺、6:右辺、7:下辺、8:左辺

これだけ設定すれば、トリガーである button をクリックすると、n で指定した位置を起終点として、トリガーボタンの直後に配置された要素が slideToggleEx 適用対象要素となり、隠蔽されたり表示されるようなコード( エントリイ末尾に掲載 )を書きました。

「 これだけ 」と言っても結構たくさんあるため、最初は面倒に感じますが、直ぐに慣れますし、実際に使ってみると決して大変な手間ではありません。

必要な HTML 構造(この部分も No.731 の再掲)

上の 5 つだけで、slideToggleEx プラグインが簡単に使えるようにするために、もう1つ該当部分の HTML 文を特別な構造にする必要があります。これが 6 つ目にして最後の要件となります。その要件とは起動ボタンの直ぐ後に slideToggleEx 対象要素があるということです。

そうでない場合には 個々に slideToggleEx メソッドを呼び出すコードを書くことになります。

該当部分の HTML を以下のいずれかにする必要があります。(いずれのケースもボックスの左上を起終点とする場合で例示します。)

■ケース 1
<div><button class="fireSlideToggleEx-1">起動ボタン</button></div>
<div>・・・slideToggleEx対象要素・・・</div>

■ケース 2
<button class="fireSlideToggleEx-1" style="display:block;width:120px;">起動ボタン</button>
<div>・・・slideToggleEx対象要素・・・</div>

※ 2 行目の div 要素は別に div でなくてもブロック要素ならば何でも構いません。

ケース 1 は一般的な button タグの使い方の場合です。つまり button タグを inline 要素として配置する場合の HTML 文です。この場合には button タグの親要素の next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

一方ケース 2 は button タグをブロック要素として配置する場合です。この場合には当該のボタンタグの next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

このような当然と思われるHTML文構成にしておけば、運用に当たってどのようにしたかを思い出すのも容易ですし、仮に構造を忘れても直感的に思い出すことが可能となります。

▲ToTop

以上の前提で作った slideToggleEx プラグイン活用コード

アニメ継続時間と easing 関数は、後述する 「 click イベントを登録し slideToggleEx メソッド適用対象をピックアップするコード 」 において、1 秒に設定し、easing は無指定としています。このコードを弄れば自在に継続時間と easing 関数は変更できますが、使う際にその都度 duration と easing 関数を変更する必要はないと考え、(1) トリガーとなるボタンタグと (2) slideToggleEx 対象のブロックタグ、及び (3) slideToggleEx-n クラス名を指定するだけで使えればよいと判断しました。

以下に掲載したコードリストの表示と隠蔽には、当然 slideToggleEx を適用しています。直下のボタンをクリックすると、 slideToggleEx メソッドが起動し、アニメ対象要素であるコードリストが当該要素の右上から展開し、再びボタンをクリックすると右上へ隠蔽されます。( 起終点= 2 )

コードには各行に詳細な説明を記述したので、何を行っているのか分かると思います。

■プラグイン slideToggleEx() メソッドコード(完全版)
  0:// jQuery plugin : slideToggleEx() Release at 2009.8.21, ver 1.1:9.9, 1.3TheLastVer:10.9 01:00am
  1:(function($){
  // アニメーションコードの性格を簡単に記述しておく
  2:var ver="relative animation under position relative";
  // プラグイン指定
  3:$.fn.slideToggleEx = function( type, duration, easing, fn ){
  4: if (type > 8) // エラー対処
  5:  (function(){
  6:   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。
      \n"+"slideToggleEx( 0~8, 継続時間, easing関数, アニメ後実行関数 )"); return;
  7:  })();
   // 変数設定、初期値複写
  8: var $target = this, o = $.extend({},$.fn.slideToggleEx.opts);
   // 9 ~ 23 行は、o オブジェクトにtargetとプロパティがない場合、
    * またはターゲットが異なった場合、つまり初めてそのインスタンスから呼ばれたか、
    * インスタンスが変わった場合に起動される。
    * このブロックはアニメ対象に対して様々な初期化を行う。
    */
  9: if(!o.target || o.target[0]!==$target[0]) {
 10:  $.extend(o,{
 11:   orig : o, // 当面使わないが念のために o をバックアップ
 12:   target : $target,
 13:   el : $target[0], // アニメ対象ノードへの参照を取得
 14:   type : type,
 15:   initstate : !$target.is(":hidden"), // アニメ前の状態が隠蔽か表示か
 16:   cnt : 1 // インスタンス毎の起動回数カウンター
 17:  });
    // アニメ対象が切り替わり、再びあるインスタンスに slideToggleEx メソッドが
    // 適用された時には、最初に呼び出された時の設定を再利用できるように data
    // メソッドを活用してコード進行の効率化を図る。
    // また o オブジェクトにアニメ対象要素等のサイズに係るプロパティを追加する。
 18:  $.extend(o, $.data(o.el,"initdone") || getSize());
    // アニメ対象ノードの固有 style 値を取得する。
 19:  for (var p in o.cssdefault.H) o.cssOrig.H[p] = $.css(o.el,p,true);
 20:  for (var p in o.cssdefault.W) o.cssOrig.W[p] = $.css(o.el,p,true);
    // 上のメソッドでは単位が付かないので付与する。
 21:  o.cssOrig.H.height +="px"; o.cssOrig.W.width +="px";
      // o オブジェクトを $.fn.slideToggleEx.opts オブジェクトに併合する。
 22:  $.extend($.fn.slideToggleEx.opts,o);
 23: }
   /* 24 ~ 36 行迄のブロックは、slideToggleEx が呼び出される度に
    * 作用する。27 行で type 値をチェックするのは、同一要素における異なる type 値
    * によるslideToggleEx メソッドの連続起動の場合に対処するためである。
    * slideToggleEx メソッドの連続起動の場合には、呼び出される度に o.duration、
    * o.easing、o.complete が変化する可能性があるので、これらも呼び出し毎に
    * 定義する必要がある。
    */
   // window サイズが変更された時には親要素サイズを計測し直す。
 24: window.onresize = getParentWH;
 25: o.cnt = $.fn.slideToggleEx.opts.cnt++; // カウンター値を 1 増する。
 26: o.odd = o.cnt % 2; // o.cnt が奇数か偶数かを判定する
 27: o.type = o.type!==type ? type : o.type;
   // duration などの初期値は $.fn.slideToggleEx.opts に用意しておく。
 28: o.duration = duration || o.duration;
 29: o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 30: o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";
 31:
   /* アニメ対象ノードに AdjustN( N は type 値 )名で
    * 関連づけられたオブジェクトがなければ、adjustcssAnim 関数を呼び出し、
    * その返値を o.cssAdjust.show 及び .hide プロパティに代入する。
    * 関連づけられたオブジェクトがあればそのまま利用する。
    * ここに o.cssAdjust は animate メソッドで利用するアニメ用 CSS プロパティの内、
    * 起終点位置に応じて変化する top 及び lef 値だけを保持するオブジェクトである。
    * show 及び hide プロパティには "+=" などの relative アニメーション値が設定される。
    */
 32: o.cssAdjust = $.data(o.el,"Adjust"+o.type) ||
 33:  $.data(o.el,"Adjust" + o.type, {show:adjustcssAnim(true), hide:adjustcssAnim(false)});
   /* アニメ対象ノードに AnimN( N は type 値 )名で関連づけられた
    * オブジェクトがなければ、setAnimCSS 関数を呼び出してその返値を o.cssAnim
    * オブジェクトに代入する。関連づけられたオブジェクトがあればそのまま利用する。
    * ここに cssAnim は animate メソッドで利用するアニメ対象プロパティの全てを
    * 保持するオブジェクトであり、29 行で取得した top値と left値も併合する。
    */
 34: o.cssAnim = $.data(o.el,"Anim" + o.type) || $.data(o.el,"Anim" + o.type, setcssAnim());
   /* アニメ対象ノードに BeforeN( N は type 値 )名で関連づけられた
    * オブジェクトがなければ、makecssBefore 関数を呼び出してその返値を o.cssBefore
    * オブジェクトに代入する。関連づけられたオブジェクトがあればそのまま利用する。
    * ここに cssBefore は animate メソッド起動直前のアニメ対象要素のスタイル属性値を
    * 取得し、あるいは設定するためのオブジェクトである。
    */
 35: o.cssBefore = $.data(o.el,"Before" + o.type) || $.data(o.el,"Before" + o.type, makecssBefore());
   // 当該のアニメ対象要素に初めて、あるいは別のアニメ対象操作後に再び
   // slideToggleEx メソッドを適用した時に、最初に起動された場合にのみ適用するコード。
   // 対象要素の表示状態に応じて、animate メソッド適用前の CSS 値を設定する。
 36: if (o.cnt==1) $target.css( o.initstate ? o.cssBefore.show : o.cssBefore.hide );
 37:
   /* アニメーション起動
    * 第 2 引数がオブジェクトではない時
    * 要素の animate メソッド適用前の表示状態と slideToggleEx メソッドの起動
    * 回数に応じて、当該メソッド第一引数である CSS オブジェクトを選択的に適用する。
    */
 38: if (typeof o.duration !=="object")
 39:  return this.animate( (o.initstate && !o.odd || !o.initstate && o.odd)
 40:   ? o.cssAnim.show : o.cssAnim.hide, o.duration, o.easing, o.complete );
   // 第 2 引数がオブジェクトの時
   // 要素の animate メソッド適用前の表示状態と slideToggleEx メソッドの起動
   // 回数に応じて、当該メソッド第一引数である CSS オブジェクトを選択的に適用する。
 41: else
 42:  return this.animate( (o.initstate && !o.odd || !o.initstate && o.odd)
 43:   ? o.cssAnim.show : o.cssAnim.hide, o.duration ); // アニメ起動
 44:// 以下は以上迄のコードから呼び出される関数群である。
   /* getSize はアニメ対象ノードの position 指定を行い、サイズを測る関数。
    * この関数は或る要素に対して slideToggleEx メソッドを適用した初回だけにたった1 回
    * だけ起動される。 54 行と 35 行の data メソッドによって data 関連づけを行わっている
    * ので、同じ要素に対する 2 度目以降の呼び出し時には、たとえ非連続的に呼び出された
    * としても、この関数は呼び出されない。
    */
 45: function getSize(){
    // position 指定を relative に
 46:  if ($target.css("position") ==="static") $target.css({position:"relative"});
 47:  var obj={}, parent={};
 48:  return obj= {
       // 親要素の幅と高さを取得
 49:   parent : {W:getParentWH().W, H:getParentWH().H},
       // アニメ対象要素のマージン辺までの高さ(算出スタイル値)取得
 50:   oH : parseInt($.css(o.el,"height",true,"margin")),
       // アニメ対象要素のマージン辺までの幅(算出スタイル値)取得
 51:   oW : parseInt($.css(o.el,"width",true,"margin")),
       // アニメ対象要素のボーダー辺までの高さ(算出スタイル値)取得
 52:   oHb : parseInt($.css(o.el,"height",true,"border")),
       // アニメ対象要素のボーダー辺までの幅(算出スタイル値)取得
 53:   oWb : parseInt($.css(o.el,"width",true,"border"))
 54:  };
 55: }
 56: function getParentWH(){ // アニメ対象の親要素のサイズを計測取得する関数
 57:  var parent = $target.parent();
 58:  return o.parent={W:parent().width(),H:parent().height()};
 59: }
   // アニメーション起終点に応じたアニメ用 top/left 値設定関数
 60: function adjustcssAnim(showhide){
 61:  var plmn = showhide ? "-=" : "+=";
 62:  return o.type==0 ? {top:plmn + o.oH/2 +"px", left:plmn + o.oW/2 +"px"} : // center
 63:   o.type==1 ? {} : // 左上端。top も left もアニメ対象プロパティとしない。
 64:   o.type==2 ? {left:plmn + o.oW +"px"} : // 右上端
 65:   o.type==3 ? {top:plmn + o.oH +"px", left:plmn + o.oW +"px"} : // 右下端
 66:   o.type==4 ? {top:plmn + o.oH +"px"} : 3aspan class="aquamarine"
 
 67:   o.type==5 ? {} : // 上辺。top も left もアニメ対象プロパティとしない。
 68:   o.type==6 ? {left:plmn + o.oW +"px"} : // 右辺
 69:   o.type==7 ? {top:plmn + o.oH +"px"} : // 下辺
 70:   {} ; // 左辺。top も left もアニメ対象プロパティとしない。
 71: }
   /* アニメ用プロパティを作成する関数
    * o.cssdefault を基にして横方向/縦方向のプロパティを作成し、またその有無を
    * type 値毎に変化させる。
    * 例えば、type 値が 5 の場合、横方向のプロパティはアニメ対象とはしない。
    * また、margin は auto が指定されている場合にはアニメ用プロパティとせず、
    * その他の場合には他のプロパティと同様に"toggle"とする。
    * 79 行で横/縦方向のプロパティを合体させ、不透明度もアニメ用プロパティと
    * して追加する。
    * 最後に 80~82 行において、それまでに作ったアニメ用 CSS オブジェクトに
    * アニメーション起終点に応じて変化する top と left プロパティを追加する。
    */
 72: function setcssAnim(){
 73:  var obj = {};
 74:  obj.W = (o.type!=5 && o.type!=7) ? $.extend({}, o.cssdefault.W,
 75:    (o.oW!==o.oWb) ? {marginLeft:"toggle",marginRight:"toggle"} : {}) : {};
 76:  obj.H = (o.type!=6 && o.type!=8) ? $.extend({}, o.cssdefault.H,
 77:    (o.oH!==o.oHb) ? {marginTop:"toggle",marginBottom:"toggle"} : {}) : {};
 78:  obj = $.extend({}, obj.W, obj.H, {opacity :"toggle"});
 79:  return o.cssAnim = {
 80:   show:$.extend({},obj,o.cssAdjust.show),hide:$.extend({},obj,o.cssAdjust.hide)
 81:  };
 82: }
   // animate メソッド適用前の適用対象要素の CSS 値を作成する関数
 83: function makecssBefore(){
 84:  var obj = {}, ret = {};
    /* margin をアニメ対象にした場合において、margin 値が "auto" の時には、
     * jquery.js の各種メソッドでサイズを測定しても margin 値が捕捉できない。
     * その結果アニメーションは予期せぬ動きをしてしまうため、まず、margin 値を
     * 強制的にゼロにし、替わりに auto 指定によって確保された margin 値に相当する
     * left 値と top 値に置き換える。これにより予期したとおりのアニメーションを
     * 引き起こさせる。
     */
 85:  // if marginLeft/Top == "auto", fix marginLeft/Top.
 86:  obj.W = (o.el.style && $.attr(o.el.style,"margin-left")==="auto") ?
 87:   {left : (o.parent.W - o.oWb)/2 +"px", marginLeft : "0px", marginRight : "0px"} :
     /* この場合も、margin 値は $.css メソッドを使って取得しないと駄目。
      * 例えば margin 指定が簡略的に margin: 10px のように行われている場合には、
      * jquery.js は margin-left、margin-right などの個々の margin 値を取得しない。
      */
 88:   {left :"0px",
 89:    marginLeft:$.css(o.el,"margin-left",true), marginRight:$.css(o.el,"margin-right",true)};
 90:  obj.H = (o.el.style && $.attr(o.el.style,"margin-top")==="auto") ?
 91:   {top : (o.parent.H - o.oHb)/2 +"px",marginTop : "0px", marginBottom : "0px"}:
 92:   {top : "0px",
 93:    marginTop:$.css(o.el,"margin-top",true), marginBottom:$.css(o.el,"margin-bottom",true)};
    // ここ迄の処理結果を show プロパティに格納する。
    // このプロパティがアニメ対象が表示されている時のアニメ前 CSS 値となる。
 94:  ret.show = $.extend({}, o.cssOrig.H, o.cssOrig.W, obj.W, obj.H);
    /* 以下はアニメ対象が隠蔽されている場合の処理
     * アニメ用CSSオブジェクトの hide オブジェクトに top プロパティが存在している
     * 場合にのみ適用する。
     * top プロパティが存在していない場合には、top プロパティはアニメ対象としない。
     */
 95:  if (o.cssAdjust.hide.top) // アニメ用 CSS オブジェクト作成関数を利用して
     // アニメ前 CSS オブジェクトを作成する。
 96:   obj.top = new Number(parseInt(obj.H.top))
 97:    + new Number(parseInt(o.cssAdjust.hide.top.slice(2)))+"px";
    // アニメ用CSSオブジェクトの hide オブジェクトに left プロパティ
    // が存在している場合にのみ適用する。
    // left プロパティが存在していない場合には、left プロパティはアニメ対象としない。
 98:  if (o.cssAdjust.hide.left) // アニメ用 CSS オブジェクト作成関数を利用
     // してアニメ前 CSS オブジェクトを作成する。
 99:   obj.left = new Number(parseInt(obj.W.left))
100:    + new Number(parseInt(o.cssAdjust.hide.left.slice(2)))+"px";
    // ここ迄の処理結果を hide プロパティに格納する。
    // このプロパティがアニメ対象が隠蔽されている時のアニメ前 CSS オブジェクトとなる。
101:  ret.hide = $.extend({}, ret.show, obj);
    // data メソッドによる関連づけを行ってから ret オブジェクトを返す。
102:  return ret;
103: }
104:} // End of $.fn.slideToggleEx
105:
106:// default 値設定と記憶用プロパティとしての機能のために
107:$.fn.slideToggleEx.opts = {
   // 既定値としての引数設定
108: duration : 1000, easing : null, complete : function(){},
   /* auto 指定への対応が必要な margin、及び type 値に応じて変化させる
    * 必要のある top と left ───これらを除くアニメ対象プロパティリスト。
    * 羅列されているプロパティは、最小値の 0 又は 1 (width と height のみ)と
    * 要素固有のプロパティ値との間でアニメーションさせることになる。
    * 高さ方向と横方向とを区別するのは、type 値に応じてアニメ対象プロ
    * パティの要/不要が変化するためである。
    */
109: cssdefault : {
110:  H : {
111:   height:"toggle",paddingTop:"toggle",paddingBottom:"toggle",
112:   borderTopWidth:"toggle",borderBottomWidth:"toggle"
113:  },
114:  W : {
115:   width:"toggle", paddingLeft:"toggle",paddingRight:"toggle",
116:   borderLeftWidth:"toggle",borderRightWidth:"toggle"
117:  }
118: },
119: cssAdjust : {},
120: cssOrig : {H:{},W:{}},
121: cssBefore : {}
122:};
123:})(jQuery);

▲ToTop

slideToggleEx Javascript コード解説

直上に掲載したコードに至るまで、実は多くのバージョンを作成し、失敗しては作り直す「らせん的」過程を何度も繰り返しました。しかし、その苦労話の前に、まずコード作成の基本的方針を述べておくべきでしょう。

slideToggleEx が想定しているシーン
  1. slideToggleEx アニメ対象ノードは表示されていても隠蔽されていても、いずれの状態でも良い。
  2. 1 つの homepage や blog エントリイ内に、複数の slideToggleEx アニメ対象ノードが存在し、それぞれのアニメ起終点は、各々同一でも良いし、異なっても良い。
  3. 1 つのアニメ対象ノードに対して連続して slideToggleEx メソッドを起動することもあり、その場合にはアニメ起終点がその都度変わることもあり得る。
  4. 独自仕様の多い IE においても起動出来るアニメーションとする。
slideToggleEx コードの効率化
  1. slideToggleEx メソッドを同一ノードに適用する場合には、コード進行過程を出来る限り繰り返さないようにする。そのために jQuery.data クラスメソッドを活用してアニメ対象ノードに情報を関連づける。
  2. 一度計算した値や使用した関数は、結果を出来る限り再利用する。そのために、jQery プロトタイプオブジェクトのプロパティを活用する。

▲ToTop

コード作成上の苦労話・強調したい点

1. メソッド終了後にも情報を保持させる措置

まず苦労したのは、slideToggleEx プラグインの再起動の際に、従前値を保持させる措置でした。

無名関数のトップレベルで、slideToggleEx メソッド外に変数を用意すれば解決することなのですが、是が非でもプラグインのメソッド外に変数を置かないでまとめたかったので、拘ってコードを作りました。その結果、メソッド外となる点では変わらないのですが、『Javascript 第 5 版』p.144 や cycle プラグインを参考にして slideToggleEx メソッドのプロパティに値を保持させることにしました。

メソッド実行が終わるとその中で定義された変数は消失してしまうので、当該メソッドの起動回数を記憶させる変数は当該メソッド外になければなりません。しかし外に変数を置くのは美しくありません。

そこでメソッドのプロパティを記憶装置にすることにしました。コードの 107 行から 122 行がその記憶装置です。

2. アニメーション開始前の対象ノードの CSS 値の処理

要素が隠蔽されていても表示されていても、要素固有の margin、border、padding、width、height 値などは保持されています。隠蔽されていてもそのスタイル諸値がゼロになるわけではありません。そのことをまず把握し理解する必要があります。( 実は私はこのことへの理解が不十分でした。 )

次に、slideToggleEx メソッドの適用対象が複数存在しているエントリイにおいて、アニメ起動が異なる要素に対してアト・ランダムに行われる場合、それぞれの要素に対して適切なアニメを起こさせることがかなり難しいことでした。

以前、slideToggleEx メソッド対象となった要素は偶々隠蔽されているかもしれないし、表示されているかもしれません。今 slideToggleEx アニメを引き起こした要素も表示されているかもしれないし、隠れているかもしれません。このようなアト・ランダムな状態においても、それぞれの要素毎に適切な slideToggleEx アニメーションが引き起こされるようにしなければなりません。

以上を踏まえた処理として cssBefore オブジェクトを用意しました(32 ~ 33 行)。このオブジェクトによりアニメーション起動前の要素のプロパティを事前に設定するわけです。これにより隠蔽状態から表示へとアニメートする要素が、適切な位置で表示されるようになりました。

3. margin 値の扱い

margin 値は一般に 「 margin:1em auto 」 などのように簡略的に指定されている場合が多いでしょう。

ところがこのような指定の場合には、jquery.js の css メソッド等を活用しても margin-left 等の個々の margin 値を取得することが出来ません。特に auto 指定がされている場合、margin をアニメプロパティに指定すると全く予想外のアニメーションが引き起こされてしまいます。jquery.js は auto 指定された margin 値を数値的に捉えることが出来ないので、margin-left、margin-right 等のプロパティをゼロと判読してしまうためです。

こうして、margin 値が様々な様式で指定されている場合でも、それを適切な数値として把握する必要が生じます。

そのための処理は makecssBefore 関数で行わせました。

▲ToTop

4. アニメ開始前における対象要素の CSS 値設定

animate メソッドは、アニメ開始前の要素の様々な属性値(スタイル属性値を含む。width、margin etc.)をアニメ開始値として採用します。(もし或るプロパティの属性値がない場合には開始値を 0 とします。)このため表示されている要素にアニメーションを適用する場合には、一般的にはアニメ前 CSS 値を設定する必要はありません。

例えばアニメ対象要素に width="380px" が設定されていれば、ブラウザがその値を解釈して、380px 幅の要素を表示しているわけです。この場合 width プロパティのアニメ開始値が何も指定されていなければ、 animate メソッドは要素固有値である 380px を width プロパティのアニメ開始値とします。

ところが隠蔽されている要素にアニメーションを適用する場合には様相が異なってきます。

隠蔽されている要素を表示するアニメーションの場合でも、その隠蔽が一般的にそうであるように display = "none" 指定によって行われている場合には、対象要素の固有の属性値は存在し続けます。要素が単に非表示になっているだけで、display = "none" 以外の属性値は表示されていた時の値が保持され続けます。

この状態に対して隠蔽要素を表示させる animate メソッドを適用すると、animate メソッドの仕様上、要素固有値がアニメ開始値になるのですから、アニメプロパティが要素固有値となってしまいます。この結果、0 からスタートさせたかったアニメーションは引き起こされません。

従って、隠蔽要素を表示させるアニメーションの場合、アニメ開始値を animate メソッド起動前に強制的に指定する必要があります。こうして強制的に CSS 値を指定するための cssBefore オブジェクトが必要・不可欠となります。

さて、その cssBefore はアニメーションの初期値を定めるわけですから、アニメーションの態様に応じて( つまり起終点位置に応じて )変化します。ボックス要素の左上、上辺、左辺などを起終点とするアニメーションの場合には問題になりませんが、例えば、アニメが要素の中心位置から始まる場合には、top と left の開始値をボックス内容辺、ボーダー辺あるいは margin 辺までの、幅や高さの半分の値に設定する必要があります。また、開始点がボックスの右上の場合には、left の開始値をボックス内容辺、ボーダー辺あるいは margin 辺までの幅に設定する必要があります。

以上に述べた隠蔽状態における cssBefore オブジェクトの作成を makecssBefore 関数の後半で行っています。

なお、アニメプロパティ値を toggle や show に指定すると、animate メソッドは要素の固有属性値をアニメの終了値として利用します。この利便性故に、アニメプロパティ値に toggle を多用することになります。

▲ToTop

5. $.data メソッドの活用

slideToggleEx プラグインでは以下の 4 箇所で $.data メソッドを利用しました。

  1. 18 行 …… $.extend(o, $.data(o.el,"initdone") || getSize());
  2. 32~33 行 ……o.cssAdjust = $.data(o.el,"Adjust"+o.type) ||    $.data(o.el,"Adjust" + o.type, $.data(o.el,"Adjust" + o.type, adjustcssAnim(false));
  3. 34 行 …… o.cssAnim = $.data(o.el,"Anim" + o.type, $.data(o.el,"Anim" + o.type, setcssAnim());
  4. 35 行 …… o.cssBefore = $.data(o.el,"Before" + o.type) || $.data(o.el,"Before" + o.type, makecssBefore());

このメソッド利用した目的は、ずばりコード進行の効率化/高速化です。このメソッドを使うことにより、不必要に同じ計算を繰り返させたくなかったのです。

上の 4 例は皆同様な形式を取っているので、どれか 1 つ分かれば他は推して知るべしです。なので、ここでは一番最後の 35 行で使った $.data メソッドについて触れておきます。

$.data(o.el,"Before" + o.type) は、アニメ対象要素 o.el に "BeforeN" ( N は type 値 )と言う名称で関連づけられたオブジェクトがある場合には、そのオブジェクト( cssBefore オブジェクト)を返し、それが左辺の o.cssBefore に代入されます。一方、関連づけられた情報がなければ false を返すので、次にある || 演算子が働きます。

|| 演算子が作用すると、オペランドである makecssBefore 関数が実行されます。

この関数では最後の行に $.data メソッドがあるので、関数によって算出したオブジェクトを、このメソッドによりアニメ対象要素 o.el に "BeforeN" 名で関連づけます。関連づけられたオブジェクトは return により左辺の o.cssBefore オブジェクトに返されます。

こうして、同一要素に対する同一 type の slideToggleEx メソッド起動時には───実際、表示/隠蔽はセットで履行されるはずだから、多くの場合 slideToggleEx メソッドは、同一要素に対して同一 type 値で繰り返し実行されます。───最初の呼び出し時にだけ makecssBefore 関数が実行され、二度目以降は固有の名称で要素に関連づけられているオブジェクト値が利用されて、 makecssBefore 関数は実行されません。これがコード進行の「最初の」「効率化」であり「高速化」です。

$.data メソッドを利用したことによる効率化は、これだけではありません。或る要素Aが slideToggleEx によって表示/隠蔽され、次に別の要素Bがやはり slideToggleEx で表示/隠蔽されたと仮定します。この後に再び要素Aに slideToggleEx メソッドを適用した場合に、更なる効率化・高速化が発揮されます。

既に $.data メソッドによるA、B各々の要素へのオブジェクトの関連づけは済んでいますから、要素Bに続けてAに slideToggleEx アニメを適用した時には、既にAに関連づけられているオブジェクトが利用され、makecssBefore 関数は起動されません。こうして無駄なコード進行が排除されます。

以上のことが 4 つの関数に対して行われるので、現在操作対象となっている頁がリロードされない限り、slideToggleEx アニメーションが一度適用された要素においては、 slideToggleEx 内にある様々な関数を殆ど実行する必要がなくなります。こうしてコード進行が大いに高速化されます。高速化は言い換えれば省エネでもあるので、流行の言葉を使えば「エコ」なコード進行が行われると言っても良いかもしれません。

▲ToTop

クリックイベント登録と slideToggleEx メソッド適用対象の簡便な登録コード

さて、slideToggleEx を使うシーンは、ボタンをクリックして或るコンテンツを表示したり隠蔽する場合が多いと思われます。そうだとすれば、その一連の操作( クリックイベントの登録・或るコンテンツの指定・slideToggleEx メソッドの要素への適用 )をコード化して半自動化してしまえば、大変楽になります。

次に述べるコードは、この一連の操作、つまりボタンへのクリックイベント登録と、クリック時の slideToggleEx メソッド適用対象の特定とそのメソッドの起動をまとめて行うものです。

具体的には 「 或るボタンがクリックされたら、slideToggleEx アニメーションが適用される或る要素が特定され、slideToggleEx メソッドが起動する 」 そのような Javascript コードです。

そのコードでは、同一ページ内に存在する、複数の同一又は異なる type の slideToggleEx アニメーションの、トリガーボタンとアニメ対象を一括して登録します。slideToggleEx の引数であるアニメ継続時間、easing 関数及びアニメ終了後の実行関数は、コードの冒頭で登録し、これを slideToggleEx メソッドに渡すようにしています。

そもそもコード作成当初では、slideToggleEx メソッドの引数を何とか HTML 文内で行いたいと考えました。クラス指定によって、アニメの起終点 ( type ) を指定するだけではなくその他の引数もまとめて指定したいと思いました。

しかし、クラス名文字列の中に、duration、easing などの文字をその都度組み込むと、冗長なクラス名指定となってしまい、いかにも面倒ですし、値だけを羅列するようにしても煩雑で順番を含めてわかりにくくなります。それにボタンクリックによる要素の表示/隠蔽操作において、その都度 duration や easing の値を変化させる必要性が高いとは思えません。

以上から、クラス名の中に slideToggleEx メソッドの引数を盛り込むことはやめました。

なお、ここにおける duration 等の指定は、ここで対象としている「 ボタンクリックによって起動される slideToggleEx 起動方法 」 において、slideToggleEx メソッドの引数を指定するものです。他方、slideToggleEx プラグインコードにおける $.fn.slideToggleEx.opts オブジェクト内でも duration 等を指定していますが、これは slideToggleEx メソッドの一般的な既定値を設定するものです。

下のボタンをクリックすると隠蔽状態にあるそのコードが、右下から展開され、再びボタンをクリックすると右下に隠れます。(type 3)

▼このコードは、メソッドプラグインではありませんが、プラグインとセットで include
 ファイルに登録しておくと重宝します。これがないと fireSlideToggleEx-n のクラス指定を
 しても next sibling 要素などを slideToggleEx の適用対象とすることは出来ません。

// button 要素へのクリックイベント登録及び slideToggleEx メソッド適用対象のピックアップ
// クリックイベント起動ボタンと slideToggleEx 適用対象を登録する。
124:// regist click Event to fire slideToggleEx() for nextBlock
125:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
   // 初期値設定
126: var duration = 1000, easing = null, complete = function(){}, clk = {};
127: function func(j){
128:  $.each(clk[j],function(i){
     // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
129:   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
130:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
131:   if ($target[i].length) {
132:    $(this).click(function(){
       // slideToggleEx を起動する。
133:     $target[i].slideToggleEx(j, duration, easing, fn);
134:     $(this).blur(); // ボタン要素のフォーカスを外す
135:    });
136:   }
137:  });
138: }
139: for (var j=0; j<9; j++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
     // jQuery インスタンスを登録する。これにより、ページ内にある
     // fireSlideToggleEx-n クラス名を持つノードを捉える。
140:  clk[j] = $(".fireSlideToggleEx-" + j);
141:    if (!clk[j].length) continue;
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
142:  func(j);
143:  clk[j].hover( // マウスオーバー/アウト時のボタン背景色の変更
144:   function(){$(this).css({backgroundColor:"pink"})},
145:   function(){$(this).css({backgroundColor:""})}
146:  )
    // マウスが押し下げられた場合の色替え
147:  .mousedown(function(){$(this).css('background-color','palegreen')})
148: }
149:});
150:});

▲ToTop

クロージャーに関するメモ

ユニークカウンター・クロージャー

よく見かけるクロージャーの例として次のカウンターがあります。

■ カウンタークロージャー
var counter = (function(){
    var id = 0;
    return function(){ return id++}
})();

// この counter 関数を次のように実行すると、その都度異なる値が返されるので、
// ユニークカウンターとして使うことが出来ます。
/* 1 度目 */  counter(); // 返値は 0
/* 2 度目 */  counter(); // 返値は 1
・・・・

このカウンタークロージャーは、外からカウンター値を弄れないため、まさにユニークな「変えられない」カウンターとして使えることから、一部では推奨されています。(『Javascript 第 5 版』等)

確かに、この counter 関数を任意のコードに組み込んでその中でカウンターとして使えば、任意のコードが呼び出された回数を確実に測定することが出来ます。(但し、そのコードを再読み込みすればゼロに戻ってしまいますが・・・)

奇数/偶数を記録するクロージャー

上を踏まえて、奇数 / 偶数をトグルで返すクロージャーを作ってみました。

■ 奇数/偶数トグルクロージャー
var state = (function(){
    var odd = 1;
    return function(){ return odd++ % 2 == 1 ? true : false }
})()

この state 関数を次のように実行すると交互に true / false が返されます。
/* 1 度目 */  state(); // 返値は true
/* 2 度目 */  state(); // 返値は false
/* 3 度目 */  state(); // 返値は true
/* 4 度目 */  state(); // 返値は false
・・・・

ここに false を偶数、true を奇数と読み替えれば、state 関数は交互に偶数と奇数を返すものとなります。しかも、その値を外から操作することは出来ません。

これをコード内で利用すれば、例えば起動回数が奇数回か偶数回かを返して、場合分けを行うことが出来ます。

▲ToTop

これらは何故クロージャーなのか?

さて、以上の 2 つのクロージャーから、何故これらが単なる関数ではなく「クロージャー」と言われるのか、その所以を学習しようと思います。

これらの 2 つの関数に共通することは、入れ子になった関数がグローバル変数に返されていることです。そのため、外側の関数の実行が終了しても、入れ子になったそれぞれの関数に対するグローバルスコープからの参照が生き続けます。

こうしてグローバル環境から、いつでも入れ子になった関数にアクセスできることになります。

なお、関数がリテラル表記されていることは本質には何ら関係ありません。次のように書いてもクロージャーであることに何ら替わりはなく、同一の挙動をするからです。リテラル表記の方が簡潔に同一のことを表現できるに過ぎません。

function state (){
    var odd = 1;
    return function(){ return odd++ % 2 == 1 ? true : false }
}
var log = state();
/* 1 度目 */  log() // result:true
/* 2 度目 */  log() // result:false
/* 3 度目 */  log() // result:true
/* 4 度目 */  log() // result:false
・・・・
クロージャーはどこにある?

ここで、まずはっきりさせなければならないことは、「 クロージャー 」と言われる部分はどこなのか、ということです。恥ずかしながら、counter 関数や state 関数がクロージャーなのか、それとも入れ子になった関数がそれなのか、しばらく理解しないまま時が経過してしまったので、ここで明らかにしたいのです。

実は、改めて Javascript 第 5 版を読み直し、また非常に分かりやすいサイト クロージャと高階関数 をじっくり読んで、やっと理解出来たのは、つい最近のことなのです。

「 クロージャーとは、その関数が定義された環境への参照を持った関数のこと 」───上記サイト『クロージャと高階関数』のこの定義が曇った頭をすっきりさせてくれました。またよくよく読み返してみると、Javascript 第 5 版 において 「 クロージャーとしての入れ子型の関数 」(p.142)と表現されているではありませんか!

つまりクロージャーは外側の関数ではなく、その中にある入れ子になった関数です。「入れ子になった関数を定義した環境」とは、外側の関数の変数とその有効範囲(スコープ)を意味するわけで、上の 2 つの例では共に、入れ子になった関数が定義された環境とは、グローバル環境下にある関数の中に存在するローカル環境です。

クロージャーは、どんな場合に 「 生き続けるか? 」 (つまり、グローバル環境から参照できるか?)

さて、入れ子になった関数はよく使われますし、その関数が外側の関数環境への参照をもっていることも決して少なくありません。例えば、次のように外側の関数の引数や内部の変数を、内側の関数から参照するようなコードを書けば、内側の関数は 「 定義された環境への参照を持った関数 」 になります。つまり内側の関数はクロージャーです。

// 関数 sample はグローバル環境にある。
function sample(x){ // グローバル関数 sample 直下に、環境 Lc1 が格納されている。
  var y = 1; // 環境 Lc1 には x、y、z の 3 つの変数がある。
  // 環境 Lc2 から環境 Lc1 の中にある x と y を参照している。
  var z = function(){ return y + x++; /*環境 Lc2*/};
  return z();
};
/* 1 度目 */  sample(2) // result:3
/* 2 度目 */  sample(2) // result:3
/* 3 度目 */  sample(2) // result:3
・・・・

しかし、いくらクロージャーがあっても、それだけではグローバル環境から内側の関数は参照出来ません。

上の例では、確かに内側の関数から外側の関数への参照( 参照 Lc2 → Lc1 )は存在しますが、グローバル環境から内側の関数への参照( 参照 G → Lc2 )が存在しません。 Lc1 への参照を持ったクロージャー(それは Lc2 環境にある)に対して、グローバル環境 G からアクセス出来なれば、外側の関数が終了すればそれと共にクロージャーもガーベージコレクションされてしまいます。

この点こそがポイントです。グローバル環境からクロージャーを参照する何らかの措置(例えば Lc2 環境からの return 値をグローバル環境に返す)を施して、グローバル環境から内側の関数にアクセス出来るようにすれば、外側の関数実行が終わった後においても、グローバル環境からクロージャーへの参照を利用して、結果として Lc1 環境へのアクセスが可能となります

模式的に書けば、G → Lc2 → Lc1 ( → は左側の環境から右側の環境への参照を表す) となるとき、Lc2 環境の関数がクロージャーになります。

多くのクロージャーサンプルは、内側の関数からの return 連鎖を利用してグローバル変数と内側の関数をリンクさせていますが、グローバル環境からの内部関数への参照を設定することが、クロージャーを効果的に利用する「 秘策 」となります。

jquery.js のアニメーションコードの活用 ( 3 ) slideToggleEx プラグインの簡便な活用のために(不完全版)

このエントリイで言及し掲載した slideToggleEx プラグインは不完全でした。完全版はエントリイ No. 733 をご覧ください。

slideToggleEx プラグインの簡便な活用方法が必要だ

jquery.js の自作プラグイン slideToggleEx を実際に各エントリイ内で随時利用するためには、簡便な利用方法を作っておく必要があります。こうした自らの需要に基づいて以下の方法をまとめました。

用意するもの・ことは以下の 5 つです。
  1. include 用 slideToggleEx プラグインファイルと include タグ要素
  2. slideToggleEx が適用されるタグ要素
  3. 起終点を右側にする場合には、対象タグ要素に width スタイル属性を設定します。また起終点を下にする場合には height スタイル属性を設定します。そうしないと animate メソッドの中で適正な幅や高さが計測されない場合があるためです。
  4. slideToggleEx を呼び出し、適用させるタグ要素(一般的には button タグでしょう)
  5. その button タグのクラス属性に fireSlideToggleEx-n を追加します。( n は 0 から 8 の整数)

n は起終点を意味し次の通りの意味を持ちます。

0:ボックスの中心、1:左上端、2:右上端、3:右下端、4:左下端、
5:上辺、6:右辺、7:下辺、8:左辺

これだけ設定すれば、トリガーである button をクリックすると、n で指定した位置を起終点として、トリガーボタンの直後に配置された要素が slideToggleEx 適用対象要素となり、隠蔽されたり表示されるようなコード( エントリイ末尾に掲載 )を書きました。

「 これだけ 」と言っても結構たくさんあるため、最初は面倒に感じますが、直ぐに慣れますし、実際に使ってみると決して大変な手間ではありません。

必要な HTML 構造

上の 5 つだけで、slideToggleEx プラグインが簡単に使えるようにするために、もう1つ該当部分の HTML 文を特別な構造にする必要があります。これが 6 つ目にして最後の要件となります。その要件とは起動ボタンの直ぐ後に slideToggleEx 対象要素があるということです。

そうでない場合には 個々に slideToggleEx メソッドを呼び出すコードを書くことになります。

該当部分の HTML を以下のいずれかにする必要があります。(いずれのケースもボックスの左上を起終点とする場合で例示します。)

■ケース 1
<div><button class="fireSlideToggleEx-1">起動ボタン</button></div>
<div>・・・slideToggleEx対象要素・・・</div>

■ケース 2
<button class="fireSlideToggleEx-1" style="display:block;width:120px;">起動ボタン</button>
<div>・・・slideToggleEx対象要素・・・</div>

※ 2 行目の div 要素は別に div でなくてもブロック要素ならば何でも構いません。

ケース 1 は一般的な button タグの使い方の場合です。つまり button タグを inline 要素として配置する場合の HTML 文です。この場合には button タグの親要素の next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

一方ケース 2 は button タグをブロック要素として配置する場合です。この場合には当該のボタンタグの next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

このような当然と思われるHTML文構成にしておけば、運用に当たってどのようにしたかを思い出すのも容易ですし、仮に構造を忘れても直感的に思い出すことが可能となります。

▲ToTop

以上の前提で作った slideToggleEx プラグイン活用コード

プラグインは position 無指定版と指定版の 2 種類を作りました。どちらも全く同様の動きをしますが、後者の方がコード全体のバランスがよいかもしれません。

当初 position 指定版を作成し、アニメ適用対象に position:relative をその都度指定していました。top と left をアニメ対象とするので、アニメ対象要素には static 以外の position 指定が為されていなければならないためです。しかし、slideToggleEx プラグインの運用を重ねるに連れ、その都度指定することが非常に面倒であることに気がつきました。

そこで改めて position 無指定版を作成したのですが、それが完成してから、ふと position 指定をコードで行えばよいことに気がつきました。そして、仮にアニメ対象に position 指定がされていない場合でも、相対指定となるよう、Javascript コードによって強制的に指定することにしました。

結果として、回り道をしてしまったのですが、このようなケアレスミスを犯すこと自体が、まだまだコード開発に不慣れである証明かもしれません。

こうして、今実際に使っている slideToggleEx プラグインメソッドは最初に作った版を改訂した position 指定版となりました。

なお、アニメ継続時間と easing 関数は、初期値を設定する slideToggleEx.opts オブジェクトにおいて、1 秒に設定し、easing は無指定としています。opts オブジェクトを変えれば、自在に継続時間と easing 関数を変更できますが、使う際にその都度 duration と easing 関数を変更する必要はないと考え、(1) 指定するボタンタグと (2) 指定されるブロックタグ及び (3) slideToggleEx-n クラス名を指定するだけで使えればよいと判断しました。

以下のコードリストの表示/隠蔽には当然ながら、slideToggleEx を適用しています。

▲ToTop

▼ver not use "position:relative/absolute" 
/* jQuery plugin : slideToggleEx() Release at 2009.8.21, ver2 at 2009.9.8*/
(function($){
var ver="absolute animation, non position assignment";
$.fn.slideToggleEx = function( type, duration, easing, fn ){
 $target=this;
 var o = $.extend({},$.fn.slideToggleEx.opts);
 // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 if(!o.target || o.target[0]!==$target[0]) {
  $.extend(o,{
   orig : o, // 当面使わないが念のために $.fn.slideToggleEx.opts をバックアップ
   target : $target, // jQuery インスタンスへの参照を登録する。
   // ボックスを show する時に初期のマージン値を使用するので記録しておく。
   initMTL:{marginTop:$target.css("margin-top"),marginLeft:$target.css("margin-left")},
   /* ボックスのマージン辺までの外枠サイズをゲットする。
    * 但し、jquery.js の 圧縮ファイルでは outerWidth(true) のように引数を指定しても
    * margin 辺までの外寸を取得しない。そこでやむなく引数を付けずに border 辺までの値を
    * 取得してから、それに margin 値を加算して margin 辺までの外寸を取得する。*/
   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
    + parseInt($target.css("margin-bottom")),
   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
    + parseInt($target.css("margin-right")), 
   // プラグイン起動回数が奇数か偶数かを記録するプロパティ
   odd : false
  });
  
  /* メモリ上の opts オブジェクトにこの上で作成したばかりの o オブジェクトを統合
   * して、初期値として保存する。同じインスタンスに対する 2 度目以降のプラグイン
   * 起動時には o オブジェクトに opts オブジェクトが転写され、かつこの if 条件は
   * falseとなるので if ブロックは履行されない。こうして効率的なコード進行となる。
   * また、2 度目以降のプラグイン起動が異なるインスタンスに対して行われた場合には
   * 2 番目の if 条件 o.target[0]!==$target[0] が true となるので、if ブロックが履行される。
   * こうして o オブジェクトも $.fn.slideToggleEx.opts オブジェクトも新しい
   * インスタンスの諸属性値を格納する。*/
  $.fn.slideToggleEx.opts = $.extend($.fn.slideToggleEx.opts,o);
 }
 
 /* 奇数回目か偶数回目か、起動回数を記録する。ここでは $.fn.slideToggleEx.opts
  * オブジェクトを利用して slideToggleEx メソッドの実行が終わった後も
  * odd 値を残しておくことがポイントである。*/
 o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;
 o.hidden = $target.is(":hidden"); // アニメ対象が隠れているかどうか
 o.duration = duration || o.duration; // 第 2 引数か opts プロパティの利用
 // 第 3 引数か opts プロパティの利用。duration がオブジェクトの時には空。
 o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 // 第 4 引数か opts プロパティの利用。duration がオブジェクトの時には空。
 o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";

 // アニメ用の CSS オブジェクトを作成する関数
 function makeAnimCSS(type,showhide){
  type = new Number(type);
  if (showhide === false) { // 隠蔽されている時のアニメ用 CSS 作成
   return type==0 ? {marginTop:o.oH/2+"px", marginLeft:o.oW/2+"px"}:// center
   type==1 ? {marginTop:0,marginLeft:0}: // 左上端
   type==2 ? {marginTop:0,marginLeft:o.oW + "px"}: // 右上端
   type==3 ? {marginTop:o.oH+"px", marginLeft:o.oW+"px"}:// 右下端
   type==4 ? {marginTop:o.oH+"px", marginLeft:0}: // 左下端
   type==5 ? {marginTop:0}: // 上辺
   type==6 ? {marginLeft:o.oW+"px"}:// 右辺
   type==7 ? {marginTop: o.oH+"px"}:// 下辺
   {marginLeft:0}; // 左辺
  // 表示されている時のアニメ用 CSS 作成
  // type 値が 5 と 7 の時にはトップ値のみ、6 と 8 の時にはレフト値のみ
  // 0 ~ 4 の時にはトップ値とレフト値を使用する。
  } else return (type==5 || type==7) ? {marginTop:o.initMTL.marginTop}
   : (type==6 || type==8) ? {marginLeft:o.initMTL.marginLeft} : o.initMTL;
 }

 if (type > 8) // エラー処理
  (function(){
   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。\n"+
   "slideToggleEx( 0~8, 継続時間, easing関数, アニメ後実行関数)"); return;
  })();
 else { // type 値が 0 ~ 8 の時
  // アニメ用 CSS の最終作成( margin 以外のプロパティ値からなるオブジェクトに、
  // makeAnimCSS 関数で作ったオブジェクトを併合する)
  var obj =$.extend( {}, // margin 以外のプロパティ群
    ( type==5 || type==7 ) ? {} : o.animWidth,
    ( type==6 || type==8 ) ? {} : o.animHeight,
    {opacity:"toggle"} );
  /* 偶数回目の起動か、アニメ対象が隠蔽時の起動の時には、makeAnimCSS の
   * 第 2 引数を true に、奇数回目の起動か、アニメ対象が表示されている時には、
   * makeAnimCSS の第 2 引数を false にする。
   * これによりアニメ用 CSS 値を二通りの場合に対応させて作成する。*/
  $.extend(obj,(!o.odd || o.hidden) 
   ? makeAnimCSS(type,true) : makeAnimCSS(type,false));
  return this.animate(obj, o.duration, o.easing,o.complete); // アニメーション起動
 }
} // End of $.fn.slideToggleEx function

/* デフォルト値を slideToggleEx メソッドの opts プロパティに設定しておく。
 * このオブジェクトは slideToggleEx メソッドの実行が終わった後の情報記録装置
 * としても機能する。*/
$.fn.slideToggleEx.opts = {
 duration:1000, easing:null, complete:function(){},
 animHeight : { // 高さに係るアニメ CSS の既定値
  height:"toggle",marginTop:"toggle",marginBottom:"toggle",paddigTop:"toggle",
  paddingBottom:"toggle",borderTopWidth:"toggle",borderBottomWidth:"toggle"
 }, animWidth : { // 幅に係るアニメ CSS の既定値
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",paddigLeft:"toggle",
  paddingRight:"toggle",borderLeftWidth:"toggle",borderRightWidth:"toggle"
 }
};
})(jQuery);

▲ToTop

▼ver use "position:relative"
// jQuery plugin : slideToggleEx() Release at 2009.8.21, verup 2009.9.9
(function($){
var ver="not absolute but relative animation on position relative";
// top と left プロパティによって起終点を操る position relative バージョン
$.fn.slideToggleEx = function( type, duration, easing, fn ){
 var $target = this;
 var o = $.extend({},$.fn.slideToggleEx.opts);
 // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 if(!o.target || o.target[0]!==$target[0]) {
  $.extend(o,{
   orig : o,
   target : $target,
   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
    + parseInt($target.css("margin-bottom")),
   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
    + parseInt($target.css("margin-right")),
   odd : false
  });
  $.extend($.fn.slideToggleEx.opts,o);
  if ($target.css("position") ==="static")
   $target.css({position:"relative",top:"0",left:"0"});
 }
 o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;
 o.hidden = $target.is(":hidden");
 o.duration = duration || o.duration;
 o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";

 function makeAnimCSS(type,showhide){ // アニメCSSプロパティを作成する関数
  var type = new Number(type);
  var plmn = showhide ? "-=" : "+="; // showhide == true → "-="
  return type==0 ? {top: plmn + o.oH/2 +"px", left: plmn + o.oW/2 +"px"} : // center
  type==1 ? {top: plmn + 0, left: plmn + 0} : // 左上端
  type==2 ? {top: plmn + 0, left: plmn + o.oW +"px"} : // 右上端
  type==3 ? {top: plmn + o.oH +"px", left: plmn + o.oW +"px"} : // 右下端
  type==4 ? {top: plmn + o.oH +"px", left: plmn + 0} : // 左下端
  type==5 ? {top: plmn + 0, left: plmn + 0} : // 上辺
  type==6 ? {top: plmn + 0, left: plmn + o.oW +"px"} :// 右辺
  type==7 ? {top: plmn + o.oH +"px", left: plmn + 0} :// 下辺
  type==8 ? {top: plmn + 0, left:plmn + 0} : {top:plmn + 0, left:plmn + 0} ;// 左辺
 }

 if (new Number(type) > 8) // エラー対処
  (function(){
   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。\n"+
   "slideToggleEx( 0~8, 継続時間, easing関数 )"); return;
  })();
 else {
  var obj = (!o.odd || o.hidden) 
   ? makeAnimCSS(type,true)  // 偶数回目の起動か、または要素が隠蔽されている時の CSS 作成
   : makeAnimCSS(type,false); // 奇数回目の起動か、要素が表示されている時の CSS 作成
  $.extend( obj, // top と left だけのCSSオブジェクトに幅と高さ関係値も追加
    ( type==5 || type==7 ) ? {} : o.animWidth, // 上辺か下辺の時には幅方向CSSは対象外
    ( type==6 || type==8 ) ? {} : o.animHeight, // 右辺か左辺の時には高さ方向CSSは対象外
    {opacity:"toggle"} ); // 透明度もアニメ対象プロパティとする
  return this.animate(obj, o.duration, o.easing, o.complete); // アニメ起動
 }
}; // End of $.fn.slideToggleEx

// 初期値設定とプロパティ記憶装置としての機能のために
// ※ これは position 無指定版と指定版に共通する同一のものです。
$.fn.slideToggleEx.opts = {
 duration:1000, easing:null, complete:function(){},
 animHeight : {
  height:"toggle",marginTop:"toggle",marginBottom:"toggle",paddigTop:"toggle",
  paddingBottom:"toggle",borderTopWidth:"toggle",borderBottomWidth:"toggle"
 }, animWidth : {
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",paddigLeft:"toggle",
  paddingRight:"toggle",borderLeftWidth:"toggle",borderRightWidth:"toggle"
 }
};
})(jQuery);

▲ToTop

▼このコードは、メソッドプラグインではありませんが、プラグインとセットで include
 ファイルに登録しておくと重宝します。これがないと fireSlideToggleEx-n のクラス指定を
 しても next sibling 要素などを slideToggleEx の適用対象とすることは出来ません。
/* regist click Event to fire slideToggleEx() for nextBlock
 * クリックイベント起動ボタンと slideToggleEx 適用対象を登録する。
 * これも position 無指定版、指定版に共通するコードです。*/
 1:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
 2: var duration = 1000, easing = null,fn = function(){}, clk = {};
 3: function func(i){
 4:  $.each(clk[i],function(){
    // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
 5;   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
 6:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
 7:   if ($target[i].length) {
 8:    $(this).click(function(){
       // IE においては右側をアニメ起終点にすると固まる事が多いので、
       // 強制的に同種の左側起終点に指定を替える。
 9:     if (jQuery.browser.msie)
10:      var k = i==2 && 1 || i==3 && 4 || i==6 && 8 || i;
11:     else k=i;
       // slideToggleEx を起動する。
12:     $target[i].slideToggleEx(k, duration, easing, fn);
13:     $(this).blur(); // ボタン要素のフォーカスを外す
14:    });
15:   }
16:  });
17: }
18: for (var i=0; i<9; i++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
    // jQuery インスタンスを登録する。これにより、ページ内にある
    // fireSlideToggleEx-n クラス名を持つノードを捉える。
19:  clk[i] = $(".fireSlideToggleEx-" + i);
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
20:   if (!clk[i].length) continue;
    // i 番名のノードが存在した場合には、当該インスタンスの
    // 各ノード毎に巡回処理を行うために func 関数を呼び出す。
21:  func(i);
22:  clk[i].hover( // マウスオーバー/アウト時のボタン背景色の変更
23:   function(){$(this).css({backgroundColor:"pink"})},
24:   function(){$(this).css({backgroundColor:""})}
25:  )
26:  // マウスが押し下げられた場合の色替え
27:  .mousedown(function(){$(this).css('background-color','palegreen')})
28: }
29:});

▲ToTop

コード作成上の苦労話

非常に苦労したのは、slideToggleEx プラグインの再起動の際に、従前値を保持させる措置でした。

無名関数のトップレベルで、slideToggleEx メソッド外に変数を用意すれば解決することなのですが、是が非でもプラグインのメソッド外に変数を置かないでまとめたかったので、拘ってコードを作りました。

その結果、メソッド外となる点では変わらないのですが、『Javascript 第 5 版』p.144 や cycle プラグインを参考にして slideToggleEx メソッドのプロパティに値を保持させることにしました。

この点は極めて重要なポイントなので、じっくりと記述することにします。

メソッド実行が終わるとその中で定義された変数は消失してしまうので、当該メソッドの起動回数を記憶させる変数は当該メソッド外になければなりません。しかし外に変数を置くのは美しくありません。

そこでメソッドのプロパティを記憶装置にすることにしました。

そのことについて具体的にコードに触れながら、以下で説明したいと思います。

▲ToTop

メソッドのプロパティに情報を記録させる方法
 1: var $target=this;
 2: var o = $.extend({},$.fn.slideToggleEx.opts);
 3: // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 4: if(!o.target || o.target[0]!==$target[0]) {
 5:  $.extend(o,{
 6:   orig : o,
 7:   target : $target,
 8:   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
 9:    + parseInt($target.css("margin-bottom")),
10:   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
11:    + parseInt($target.css("margin-right")),
12:   odd : false
13:  });
14:  $.fn.slideToggleEx.opts = $.extend($.fn.slideToggleEx.opts,o);
15:  if ($target.css("position") ==="static")
16:   $target.css({position:"relative",top:"0",left:"0"});
17: }
18: o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;

上の 18 行のコードは slideToggleEx プラグイン( position 指定版 )の最初の部分から抽出したものです。

  1. まずインスタンスへの参照を $target 変数に登録します(#1)。

    これはインスタンスへの参照が必要な場合にその都度 this を使うと、シーンによっては参照先が変わってしまうことがあるので、何らかの変数が必要だからです。別に self でも良いのですが、cycle プラグインに倣って jQuery インスタンスへの参照であることが視覚的にわかるように $ を使い、また target とすることによりアニメ対象であることを意味的にも表示するようにしました。

  2. 次にメソッドの opts プロパティを o オブジェクトに転写します(#2)。

    この行は極めて重要な役割を果たします。これで o オブジェクトに初期値が設定されると共に、同じインスタンスからの 2 度目以降の slideToggleEx の呼び出しの際には、初回呼び出し時に設定された諸値が、初回呼び出し時の 14 行の実行によって、opts プロパティに登録されているので、情報満載のその opts が o に複写されます。

    この結果、4 行目の if 文において、o.target プロパティは存在するし、o.target[0] === $target[0] となるので、4 ~ 17 行がパスされます。こうして同じインスタンスに対して、諸値設定を繰り返させない効率化を実現しました。

  3. 他方、slideToggleEx の最初の呼び出しの際、またはインスタンスが変わった時の呼び出し時には、o.target プロパティが存在しないか、o.target[0] と $.target[0] は異なるため、4 行目の if 文が成立し、5 ~16 行が実行されます。

    まず、opts オブジェクトの初期値をバックアップするために、それが転写された o オブジェクトを orig プロパティに保存します(#6)。このバックアップは当面使いませんが、何らかの必要が後で生じるかもしれないとの老婆心です。

    次に、target プロパティに $target への参照を登録します(#7)。これにより上で述べたような 二度目以降の呼び出し時の if 文不成立を成り立たせます。

  4. o.oH と o.oW には インスタンスに登録されている最初の要素の外寸を登録します。これも 1 回だけ測ればよいので if 文の中に入れました(#8 ~ 11)。
  5. o オブジェクトの最後のプロパティは起動回数を記録するためのプロパティです。これは 18 行との関係で false とする必要があります(#12)。
  6. 以上により初回呼び出し時にだけ、インスタンスに係る諸情報を設定・獲得し、その値を 2 行目で得た o オブジェクトに上書きし(#5)、その直後にその拡張された o オブジェクトを opts オブジェクトに併合します(#18)。こうすることにより、o オブジェクトが持つ情報が opts オブジェクトに複写・保存され、slideToggleEx が実行を終えても、当該インスタンスに係る情報を slideToggleEx.opts プロパティが保持し続けます。
  7. 15 ~ 16 行では 当該インスタンスに登録された要素の position 指定をチェックし、relative になっていなければそのように設定します。

    こうすることにより slideToggleEx 対象要素を作成する時には一々 position 指定をしなくても良いようにしました。なおこの処置も当然 1 回だけ行えばよいので、if 文の中で行います。

  8. 最後の 18 行は、起動回数を記録するためのコードです。

    12 行で false とされた o.odd 値は 14 行によって opts.odd = false と複写されます。そして 18 行でこれを否定することにより opt.odd は trueとなり、これが o.odd にも代入されます。

    こうして初回起動時の o.odd 値は true となり奇数回目であることが登録されます。二度目の呼び出し時には 2 行からいきなり 18 行に飛ぶので 18行の o.odd は false、つまり偶数回目となります。

    ここにおいて、常に opts.odd に偶数/奇数回数を示す値が保持され続け、そこから o.odd 値が複写され、呼び出しの都度変更されることがポイントです。

  9. 最後に、slideToggleEx を呼び出すインスタンスが変わった場合のコード進行を説明します。

    この時には if 文の 2 番目の条件が効果を発揮します。起動元インスタンスが変われば、2 行目によってゲットされる o.target はそれ以前の別のインスタンスへの参照を保持しているため、必ず o.target[0] は $.target[0] と異なります。こうして if 文が成立し、改めて起動元インスタンスの要素サイズ( o.oH と o.oW )が計測されます。

    そして、その結果は 14 行で opts オブジェクトに複写され、当該インスタンスからの二度目以降の呼び出し時には改めて o オブジェクトに複写されます。

    こうして以前のインスタンスに登録されていた要素のサイズ計測結果は、上書きされて消失し、新しい現在の呼び出し元インスタンスに登録されている要素のサイズが opts オブジェクトに保持され、o オブジェクトに登録されます。

▲ToTop

クリックイベント登録と slideToggleEx 対象要素を特定するためのコードについて

このコードはプラグインメソッドではありませんが、容易に slideToggleEx プラグインを利用するために不可欠なものです。そして作成にかなり苦労を重ねた難産コードでした。そこで一節を設けて詳細に説明しておきたいと思います。

■ クリックイベントと slideToggleEx 適用対象を登録するためのコード ( 再掲です )
/* regist click Event to fire slideToggleEx() for nextBlock */
 1:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
 2: var duration = 1000, easing = null,fn = function(){}, clk = {};
 3: function func(i){
 4:  $.each(clk[i],function(){
     // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
 5;   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
 6:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
 7:   if ($target[i].length) {
 8:    $(this).click(function(){
       // IE においては右側をアニメ起終点にすると固まる事が多いので、
       // 強制的に同種の左側起終点に指定を替える。
 9:     if (jQuery.browser.msie)
10:      var k = i==2 && 1 || i==3 && 4 || i==6 && 8 || i;
11:     else k=i;
       // slideToggleEx を起動する。
12:     $target[i].slideToggleEx(k, duration, easing, fn);
13:     $(this).blur(); // ボタン要素のフォーカスを外す
14:    });
15:   }
16:  });
17: }
18: for (var i=0; i<9; i++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
    // jQuery インスタンスを登録する。これにより、ページ内にある
    // fireSlideToggleEx-n クラス名を持つノードを捉える。
19:  clk[i] = $(".fireSlideToggleEx-" + i);
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
20:   if (!clk[i].length) continue;
    // i 番名のノードが存在した場合には、当該インスタンスの
    // 各ノード毎に巡回処理を行うために func 関数を呼び出す。
    // ここに func 関数の第 1 引数 clk[i] が外側の関数内にある変数 clk への
    // 参照を保持しているので、func 関数はクロージャーとなっている。しかし、
    // グローバル環境から func 関数への参照はないので、クロージャーを通じた
    // ローカル変数の活用は行っていない。
21:  func(i);
22:  clk[i].hover( // マウスオーバー/アウト時のボタン背景色の変更
23:   function(){$(this).css({backgroundColor:"pink"})},
24:   function(){$(this).css({backgroundColor:""})}
25:  )
26:  // マウスが押し下げられた場合の色替え
27:  .mousedown(function(){$(this).css('background-color','palegreen')})
28: }
29:});

要点は以下の通りです。

  1. 一般的に同一ページ内で複数の slideToggleEx 対象があるのは当たり前であり、更に同一の起終点から起動させる slideToggleEx 対象が複数ある場合も決して少なくないこと。
  2. 従って class 名を頼りに click イベントを登録するものの( 使う度に決めなければならない id を頼りにすることは出来ません。)、同一 class 名の要素がそれぞれ個別に識別できなければなりません。さもないと、1 つのボタンをクリックした際に、同一クラス名を有する slideToggleEx 対象ノードの全てが一斉にアニメートしてしまいます。

以上の要点を踏まえたコードは結構複雑な構成になりました。

  1. (#2) イベント登録オブジェクト clk を用意します。
  2. (#3 ~ 17) 個々のノードに対してクリックイベントを登録する関数を定義します。この関数を設けたことには大きな意味があります。関数化しないで 21 行の位置に each メソッドを置くと、for loop 内にあるために、どんな場合でも slideToggleEx の第 1 引数が 9 となってしまうからです。
  3. (#18) 9 回の巡回処理により、クラス名を頼りに、個々のノードに対してクリックイベントを登録します。
  4. (#19)まず、クリックイベントを登録するオブジェクト clk の i プロパティを作成し、それに "fireSlideToggleEx-i"( i は 0~8 ) class のノードをもつ jQuery インスタンスを登録します。このインスタンスは複数のノードを含んでいる場合もあり得ます。
  5. (#20) ノードが 1 つも含まれなければ、i が 1 増されてループの最初から処理が再開されます。
  6. (#21) func(i)を呼び出します。この関数が個々のノードに対してクリックイベントを登録します。
  7. (#4) clk[i] インスタンスに登録されたノードの数だけ each 巡回処理を繰り返します。
  8. (#5) $target オブジェクトはその i プロパティに slideToggleEx 対象の個々のノードを 1 つずつを格納するオブジェクトです。
  9. (#6) this は clk[i] に登録されている 1 つのノード を意味します。その jQuery インスタンス $(this) の display style 値を調べて、next sibling 要素への参照を $target[i] プロパティに登録します。
  10. (#7) 返されたインスタンスに slideToggleEx 対象となるノードが 1 つ以上あれば 8 行目以降が実行されます。
  11. (#8) 個々の要素 ( $(this)、つまり clk[i] に登録されている各ノード) に click イベントを登録します。こうして個々の要素毎に、個々の slideToggleEx 対象が特定されてクリックイベントが登録されます。
  12. (#9 ~ 10) IE の場合ブロックの右側を起終点とするアニメーションは固まる場合が多々あります。

    そこで IE に限って右側の起終点を利用せずに、同等の左側起終点となるように変換します。

  13. (#11) IE 以外のブラウザの場合の処理です。
  14. (#12) $target[i] から slideToggleEx メソッドを起動します。
  15. (#22 ~ 27) これらの行は一般的なコードです。

jquery.js のアニメーションコードの解読 ( 10 ) 番外編 easing関数解読

easing 関数<解読>

解読とは大袈裟すぎるかもしれませんが、一見複雑に見える easing 関数式ですが、実は中学生程度の関数の知識があれば十分理解できるものであることを、恥ずかしながら最近納得したばかりなのです。

そこで、easing 関数が実は簡単な代数式であることを改めて整理しておこうと思います。それも中学校で倣う関数の形式に、表現し直してみます。

対象とする easing 関数は jquery.js で定義されている linear 及び swing の他、George Smith 氏による 30 種類の easing 関数です。

以下の表における大前提
  1. 数値の正規化

    大前提として、jquery.js における正規化を踏まえるので、easing 代数式の文字の意味は次のようになります。

    t: current time :これは経過時間ですが、jquery.js においては、t を d で割った経過時間推移率とでも呼ぶべき値が t に代入され、それがグラフの横軸変数値となります。

    b: begInnIng value : 初期値は jquery.js では常に 0 です。b = 0

    c: change In value : 変動値は jquery.js では常に 1 です。c = 1

    d: duration : 継続時間ですが、関数式の中には直接登場することは殆どありません。

  2. easeIn と easeOut の関係

    x → 1-x, y → 1 - y へと座標変換を施すことによって、easeIn 関数から自動的・反射的に easeOut 関数が導き出せます。

  3. easeInOut 関数の作り方

    この関数は、経過時間推移率が 0.5 以下の場合には easeIn 関数を、それ以上の時には easeOut 関数を適用するだけのことです。

  4. 解読は easeIn だけで十分

    つまり、easeIn 関数が分かれば、座標変換と場合分けによって、簡単に easeOut 関数とeaseInOut 関数が出来てしまうので、easeIn 関数だけ<解読>すれば必要にして十分です。

見慣れた関数式への変換

以下の式において、x は時間推移率( t/d )を、y は easing 関数値を表しています。b,c,t,d は easing 関数で定められている通りの定義です。上に述べた事を踏まえて b = 0, c = 1, t/=d → x としました。

関数名Javascript関数式見慣れた関数式
linearb + c * ty = x
swing((-Math.cos(t * Math.PI)/2) + 0.5) * c + b y = -( cos(x * π) ) / 2 + 0.5
easeInQuadc*(t/=d)*t + by = x 2
easeInCubicc*(t/=d)*t*t + by = x 3
easeInQuartc*(t/=d)*t*t*t + by = x 4
easeInQuintc*(t/=d)*t*t*t*t + by = x 5
easeInSine-c * Math.cos(t/d * (Math.PI/2)) + c + by = - cos(x * π/2) + 1
easeInExpo(t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + by = 2 10 * ( x-1 ),
x が 0 の時は y = 0
easeInCirq-c * (Math.sqrt(1 - (t/=d)*t) - 1) + by = - (1 - x * x)0.5 + 1
easeInBackif (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; y = x * x * ( 2.70158 * x - 1.70158 )
easeOutBounce((t/=d) < (1/2.75))
 c*(7.5625*t*t) + b;
(t < (2/2.75))
 c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
(t < (2.5/2.75))
 c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
else
 c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
・x < 1/2.75 → 7.5625* x * x
・x < 2/2.75 → 7.5625* (x-1.5/2.75) * (x-1.5/2.75) +0.75
・x < 2.5/2.75 → 7.5625* (x-2.25/2.75) * (x-2.25/2.75) +0.9375
・x >= 2.5/2.75 → 7.5625* (x-2.625/2.75) * (x-2.625/2.75) +0.984375
easeInElastic var s=1.70158;var p=0;var a=c;
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
if (a < Math.abs(c)) { a=c; var s=p/4; }
else var s = p/(2*Math.PI) * Math.asin (c/a);
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
作成中

jquery.js のアニメーションコードの活用 ( 2 ) アニメを使ってグラフを動的に描く

アニメーションを使ってグラフを描く

アニメーション活用エントリイの 2 つ目は、アニメーションを活用してグラフを書くコンテンツです。

そもそも、easing 関数のグラフ化を考えていたのですが、エクセルなどで作って貼り付けるのは余りにも芸がないと思われ、何とか Web サイト上で動的にグラフが描けないだろうか、と前から思っていました。

そして jquery.js のアニメーション解読を進めていく内に、easing 関数それ自体に興味を持ったことと併せて、アニメーションの options.step メソッドを使えば、アニメ途中の情報を取得できるのでグラフが書ける───入浴中にふと(苦笑)、そのことに気付きました。

構想ニ 1 日間、コーディングに約 2 日を要して( と言っても勤めながらなので、夜と朝だけのマイタイムですが )、何とか具体的な成果に漕ぎ着けました。

以下がその結果ですが、何はともあれ、アニメ継続時間を 2 秒か 4 秒程度にし、適当な easing 関数を選択してから、グラフ作成ボタンをクリックしてみてください。

僅か 3 クリックで、今選択した継続時間を要して青いボックスが左下に閉じられ、その後また同じ継続時間を使って右上方向に開かれます。これで「 グラフの座標 」が完成します。

そしてボックスが開き終わると、数10ミリ~数100ミリ秒後にそのボックス内に、今適用された easing 関数のグラフが描かれます。このときボックス下辺はグラフ横軸となって継続時間の推移割合を示し、、ボックス左辺は縦軸となり easing 関数値を表しています。

なお、グラフの大きさは相対的な問題に過ぎません、以下の例示の場合には、青いボックスの大きさ(つまりグラフ座標)を、レイアウト上の必要性から偶々 350 px 角としました。その結果X軸、Y軸両方向とも、時間推移割合と関数値を 350 倍してグラフを作成しています。

【注意】
IE では関数値が 0 未満または 1 を越える easing 関数の場合、固まってしまうようです。ですから Back、Elastic などは IE ではグラフ描画は出来ませんが、もし固まった場合には、この頁をリロードすれば、Javascript の 「 凝固状態 」 を解除出来ます。

  1. まず選択した easing 関数の効果を、ボックス隠蔽 / 表示の 2 つのアニメーションを通じて確認してください。
  2. ボックスが開き終わると直ぐに、今選択し、ボックスの開閉に利用した easing 関数のグラフが、開き終わったボックス内に描かれます。
アニメ継続時間
適用 easing

▲ToTop

グラフ描画の仕組み

jquery.js における easing 関数の扱い
easing 関数とは

「 easing 」 は速度を増減するときに使うもの、とされています。( イージング (Easing) とは | FLASH関連用語集 | ミツエーリンクス )。つまり easing 関数は動きを加速し、あるいは減速する加速度の役割をもっています。

そして、横軸に時間を、縦軸に変動値を取って「 動き 」をグラフ化した場合において、それが直線であれば等速で動いたことを表し、直線の角度は速度の高低を意味します。そして直線でない曲線部分は、加速度が働いた部分であり、上に凸ならば減速、下に凸ならば加速となります。

さて、easing 関数は Tween クラスの場合も jquery.js の場合も、基本的に次の 4 つの変数により変化する二次元関数です。初期値(b)、変動幅(c)、アニメの継続時間(d)、そして現在までの経過時間(t)です。つまり、中学校時代に倣った馴染みの関数式 y = f (x) になぞらえれば、easing 関数の基本形式は次のようになります。

変動後の値 = f ( 初期値, 変動幅, 経過時間, 継続時間 )

ここにおいて時間と共に変化する変数はただ 1 つ 経過時間であり、後は全て定数値となります。勿論、変動幅を時間の関数にすれば、それも変化することになりますが、その場合であっても変動幅は経過時間の関数であり、結局経過時間が唯一の変数となります。

jquery.js の easing 関数

jquery.js では変動幅( c : change で表される場合が多い )を正規化しています。つまり 0 から始まり 1 で終わるように引数が設定されています(jquery.js ver1.32 #4139)。これにより関数はいわば純化され、事の本質が分かりやすくなっていますし、そのことが easing 関数への理解を深める一助ともなっていると思います。

jquery.js における easing 関数の詳細(plugin 30種類を含む)

jquery.js に含まれている easing 関数は linear と swing の 2 類だけですが、それに30種類を追加するプラグイン ( George Smith 氏による 30 種類の easing 関数 ) があります。

エントリイを改めて、これらの 32 種類の easing 関数の全貌を、昔習った数学の関数式を思い出しながら解明しようと思っています。

▲ToTop

どのように関数値を取得するか

options.step メソッドを使います。

私が今自宅で使用しているパソコンでは、 Firefox でこの頁を開いて計測してみたところ、jquery.js の内部メソッドである e.step メソッドは、1 つのアニメ対象要素の 1 つのアニメ対象プロパティ当たり、1 秒間に約 80 回実行されます。(IE の場合、IE 8 であっても回数はもっと少なくなります。Javascript 実行速度が遅いためです。)

このそれぞれの瞬間毎に、 animate メソッドの引数として指定する options.step メソッドを利用して、( jquery.js の内部で作成される ) e インスタンスに保持されているアニメ諸情報を取得します。

easing 関数値は e.pos プロパティに保持されていますので、これを取りだして配列に格納し、その後その配列の各要素を使って、e.step が間歇的に取得した情報から、取得しなかった中間値を補間する計算を行わせて、空隙部を埋めます。

こうして横軸の全てのピクセル数 350 個に対応する関数値を取得又は推定し、グラフ化します。

グラフは 1 × 1 dot の点を 順に青いボックス内に append することにより描きます。

詳細はコード解説部分で触れます。

options.step メソッドで取得できない座標値の補間方法

「 無視できるほど小さい部分では曲線は直線と見なすことが出来る 」という微分の考え方に拠りました。決して複雑ではなく、つまりは単なる直線補間です(苦笑)。

当然アニメ継続時間を長くすればするほど、stepメソッドの呼び出し回数が増えるので、補間値が減り、step メソッドによる算出値が増え、グラフは正確さを増します。

このことは継続時間を長短変えてグラフを描かせてみると良く分かります。継続時間が短いとグラフは直線部分が増え、長くすると直線が減っていきます。これは計算により算出した補間値の数が減るためです。

この件に関する詳細もコード解説部分で触れます。

コーディングの苦労話

ここで行ったことは、ボックスアニメーションを引き起こしつつ、そのボックス内にそのアニメに使用した easing 関数のグラフを書かせるという複合的なアニメーションです。

それ故に、コーディングではグラフを描かせるタイミングについて、幾つかテストしながら調整しました。辿り着いたのは、options.step メソッドの最中でもなく、完全にアニメが終わってからでもなく、step メソッドによる情報取得が終わった段階で直ぐに補間値算出を始めさせ( この段階ではアニメーション動作は進行中です )、かつその補間値計算が終わったら直ぐにグラフ描画を始めさせる、という併行的なコード進行でした。

補間値計算やグラフのプロット作業がアニメーションの進行に干渉せず、かつアニメ完了後出来るだけ短時間でグラフ描画を行わせる最適な方法を探った結果こうなりました。

次に、2 つのバージョンを作った経緯を書いておきたいと思います。

最初に作成したのは以下に掲げた Ver 1 です。このコードは良く見渡すと、drawGraph メソッドが呼ばれる度毎に、アニメ対象の高さ・幅を計算し、隠蔽/表示用アニメ CSS を作り、duration・easing・complete を算出し、step メソッドを登録しています。これはいかにも無駄です。

そこで二度目以降の drawGraph メソッド呼び出し時には、初回呼び出し時に算出した各値を可能な限り再利用するようなコード進行に変更すべきだと思い、Ver 2 を作りました。

▲ToTop

プラグイン drawGraph() メソッドコード
■プラグイン drawGraph() メソッドコード
// ▼ Ver 2 : コード進行の効率化を考慮した改訂版
 1:(function($){
 2:$.fn.drawGraph = function(d,e,fn){
 3: var $box=this, o={}, cnt=0, allsteps=0,layout=[], dotAry=[], arrgAry=[];
   // メソッド実行後も o オブジェクトのプロパティ値を保持させるために、
   // drawGraph メソッドの opts プロパティを作る。
 4: if (!arguments.callee.opts) arguments.callee.opts={};
   // drawGraph メソッドの opts プロパティがあれば、o オブジェクトに併合する。
   // これにより 2 度目以降の drawGraph 呼び出し時に o オブジェクトに opts が複写される。
   // その opts は、直前の drawGraph メソッド実行時に 6 ~ 19 行で作成された o オブジェクトが
   // 20 行において複写されたものなので、2 度目以降の drawGraph 呼び出し時には、
   // 直前実行時に作成したプロパティが o オブジェクトに格納される。
 5: else $.extend(o, arguments.callee.opts);
   // box プロパティがないか(初回呼び出し時)、登録されているノードが異なる場合
   //(今回の使い方では呼び出し元は常に同一なので、ノードが異なることはないが
   // 一般化するために敢えて後者の条件を書いておく。)
 6: if (!o.box || o.box[0] !== $box[0] ) {
 7:  o.box = $box; // this への参照を登録する
 8:  o.log = $box.next(); // log 表示用ノードへの参照を獲得する。
 9:  o.h = $box.height(); // アニメ対象ノードの高さを測定する。
10:  o.w = $box.width(); // アニメ対象ノードの幅を測定する。
11:  o.hideCSS = {height:"toggle",width:"toggle",top:"+="+o.h+"px"}; // 隠蔽アニメ用CSS
12:  o.showCSS = {height:"toggle",width:"toggle",top:"-="+o.h+"px"}; // 表示アニメ用CSS
13: }
   // duration プロパティがないか、あっても引数と異なる場合にのみ 引数 d を o.duration に登録する。
14: if (!o.duration || o.duration !== d) o.duration = d;
   // easing プロパティがないか、あっても引数と異なる場合にのみ 引数 e を o.easing に登録する。
15: if (!o.easing || o.easing !== e) typeof o.duration ==="object" ? "" : o.easing = e;
   // fn が存在して関数で complete プロパティがないか、complete が fnと異なる場合
   // fn を o.complete に代入し、その他の場合には何もしない関数を代入する。
16: o.complete = typeof o.duration ==="object" ? "" :
17:  fn && $.isFunction(fn) && (!o.complete || o.complete!==fn) ? fn : function(){};
   // stepObj は毎回作成しないと doStep が初期化されない。
18: o.stepObj = typeof o.duration ==="object" ? $.extend({},o.duration,{step:doStep}) :
19:  {duration:o.duration, easing:o.easing, complete:o.complete, step:doStep};
   // 以上迄で作成された o オブジェクトを opts に複写し、2 度目以降の drawGraph
   // メソッド起動時に o オブジェクトの複写元とする。
20: $.extend(arguments.callee.opts, o);
21:
22: function doStep(){ // step メソッドを使ってグラフの横軸値と縦軸値を取得する
23:  var e = arguments[1]; ++allsteps; // e.options.step の第 2 引数である e オブジェクトを代入
24:  if (e.prop!=="height") return; // height プロパティ以外の時には何もしない。
25:  ++cnt ;
26:  layout[cnt]={};
27:  layout[cnt]["top"] = ( 1-e.pos ) * o.h; // グラフの縦軸値( easing 関数値 )
28:  layout[cnt]["left"] = e.state * o.w; // グラフの横軸値( 時間推移率 )
29:  dotAry.push(layout[cnt]); // layout 配列の要素を dptAry 配列に格納する。
    // 時間推移率が 100 %以上になったら直線補間関数を呼び出す。
30:  if (e.state >= 1) arrangeGraph();
31: }
32: function arrangeGraph(){ // 直線補間関数
33:  var len = dotAry.length;
34:  while (len){
     // dotAry の先頭要素を削除して arrgAry 配列に格納
35:   arrgAry.push(dotAry.shift());len--;
     // dotary 配列の次の要素のleft値と、格納されたばかりの dotstack 配列要素の left 値の差をとり
36:   if (!len) break;
37:   var diffLeft = Math.round(dotAry[0].left) - Math.round(arrgAry[arrgAry.length-1].left);
     // 差がなければ同一値と見なして dotary 配列の先頭要素を削除
38:   if (diffLeft == 0) {dotAry.shift();len--;}
     // 差が 1 ならば次の値なので dotAry 配列の先頭要素を削除し、それを arrgAry 配列に追加する
39:   else if (diffLeft == 1) {arrgAry.push(dotAry.shift());len--;}
40:   else {// 差が 2 以上あれば、補完値算出用に、その差で top 値を割って平均値を出す
41:    var avr = (dotAry[0].top - arrgAry[arrgAry.length-1].top) / diffLeft;
42:    for (var j=1, obj={}; j<diffLeft ;j++){
43:     // 差の数-1 回だけ補完値を算出し、arrgAry 配列に追加する。
44:     obj ={
45:      top: arrgAry[arrgAry.length-1].top + avr, 
46:      left: ++arrgAry[arrgAry.length-1].left
47:     };
48:     arrgAry.push (obj);
49:    }
50:   }
51:  }
52:  plotGraph();
53: }
54: function plotGraph(){ // ボックス内にグラフを描く
55:  $box.append("<div id='selectedEasingName' style='position:absolute;top:5px;left:15px;'>"+ o.easing +"</div>");
56:  o.log.html("height prop の step 起動回数 :"+ cnt +"<br />全ての step 起動回数:" + allsteps);
57:  for (var i=0, len = arrgAry.length; i<len; i++){
58:   $("<div class='dot hack' />").css({top:arrgAry[i].top+"px",left:arrgAry[i].left+"px"}).appendTo($box);
59:  }
60: }
61:
62: $box.empty(); o.log.empty(); // 表示されている easing 名と log を消す。
   // 隠蔽及び表示アニメを引き起こす。表示アニメの時にグラフも描く。
63: return $box.animate(o.hideCSS,o.duration,o.easing ).animate(o.showCSS, o.stepObj);
64:};
65:})(jQuery);
// ▲ Ver 2 終わり

// ▼ Ver 1 :サイズ測定と o オブジェクト作成を毎回行っている非効率版
(function($){
$.fn.drawGraph=function(duration,easing,fn){
 var cnt=0, allsteps=0, $box=this, layout=[], dotAry=[], arrgAry=[],
   o = $.extend({},{duration:duration, easing:easing});
 $box.h=$box.height(); $box.w=$box.width(); $log=$box.parent().next();
 var hideCSS =  {width:"toggle",height:"toggle",top:"+="+$box.h+"px"},
   showCSS =  {width:"toggle",height:"toggle",top:"-="+$box.h+"px"};
 o.step = doStep;
 o.complete = function(){$.isFunction(fn) ? fn() :function(){} };

 function doStep(){
  var e = arguments[1]; ++allsteps;
  if (e.prop!=="height") return;
  ++cnt ;
  layout[cnt]={};
  layout[cnt]["top"] = ( 1-e.pos ) * $box.h;
  layout[cnt]["left"] = e.state * $box.w;
  dotAry.push(layout[cnt]);
  if (e.state >= 1) arrangeGraph();
 }

 function arrangeGraph(){
  var len = dotAry.length;
  while (len){
   // dotAryの先頭要素を削除して arrgAry 配列に格納
   arrgAry.push(dotAry.shift());len--;
   // dotary 配列の次の要素の left 値と、格納されたばかりの dotstack 配列要素の left 値の差をとり
   if (!len) break;
   var diffLeft = Math.round(dotAry[0].left) - Math.round(arrgAry[arrgAry.length-1].left);
   // 差がなければ同一値と見なして dotary 配列の先頭要素を削除
   if (diffLeft == 0) {dotAry.shift();len--;}
   // 差が 1 ならば次の値なので dotAry 配列の先頭要素を削除し、それを arrgAry 配列に追加する
   else if (diffLeft == 1) {arrgAry.push(dotAry.shift());len--;}
   else {// 差が 2 以上あれば、補完値算出用に、その差で top 値を割って平均値を出す
    var avr = (dotAry[0].top - arrgAry[arrgAry.length-1].top) / diffLeft;
    for (var j=1, obj={}; j<diffLeft ;j++){
     // 差の数 - 1 回だけ補完値を算出し、arrgAry 配列に追加する。
     obj ={
      top: arrgAry[arrgAry.length-1].top + avr, 
      left: ++arrgAry[arrgAry.length-1].left
     };
     arrgAry.push (obj);
    }
   }
  }
  plotGraph();
 }

 function plotGraph(){
  $box.append("<div id='selectedEasingName' style='position:absolute;top:5px;left:15px;'>"+ o.easing +"</div>");
  $log.html("height prop の step 起動回数 :"+cnt+"<br />全ての step 起動回数:" + allsteps);
  for (var i=0, len = arrgAry.length; i<len; i++){
   $("<div class='dot' />").css({top:arrgAry[i].top+"px",left:arrgAry[i].left+"px"}).appendTo($box);
  }
 }

 $box.empty();$log.empty();
 return $box.animate(hideCSS,duration,easing ).animate(showCSS, o);
};})(jQuery);
// ▲ Ver 1 終わり

// drawGraph プラグインを使って実際にグラフを書かせるためのコード
(function($){
 var selDuration=2000,selEasing="swing";
 // アニメ継続時間コンボから 選択された duration を取得
 $("#sel1_728").change(function(){selDuration = +$(this).val();});
 // easing リストから 選択された easing 関数を取得
 $("#sel2_728").change(function(){selEasing = $(this).val();});
 $("#btn1_728").click(function(){$("#box1_728").drawGraph(selDuration,selEasing);});

 $("input").mouseup(function(){$(this).css({backgroundColor:""}).blur();})
  .hover(function(){$(this).css({backgroundColor:"pink"});}
  ,function(){
    $(this).css({backgroundColor:""});
  })
  .mousedown(function(){$(this).css({backgroundColor:"lightgreen"})});
})(jQuery);

▲ToTop

drawGraph() メソッド Ver 2 に関する補足説明

直前起動時に作成したアニメ対象に関する諸情報を出来る限り 2 度目以降に活用する───これが Ver 2 作成の動機であり目的です。『 Javascript 第 5 版 』p.144 を参考にして、メソッドのプロパティを活用する方法を作ったまでは良かったのですが、なかなか順調に動きませんでした。18 行目の stepObj も他のプロパティと同様に初回作成時の値を使うコードを書いたのですが、これが躓きの原因でした。

14~17 の 4 行では、o.duration、o.easing、o.complete の各プロパティの存在を確認し、それがあれば何もしないようなコードを書きました。こうしてコード進行の効率化を図ったのでした。

そしてそれ自体は問題なく動いたので、安心して 18 行も if (o.stepObj) 文を書いてしまったのです。

ところが、こうすると 2 度目以降においては o.stepObj は再定義されないので、初回時の値をそのまま保持し続けてしまいます。その結果 3 行目の cnt = 0 が 2 度目以降に読み込まれ初期化されても、その後の過程で opts プロパティに o オブジェクトを複写・待避させているため、o.stepObj 内の cnt は初期化されず累積されてしまうのです。

そのことが分かるまでに数時間を要したのですが、大きな落とし穴に落ち込んでしまった結果、貴重な経験を重ねることが出来ました。

▲ToTop

jquery.js のアニメーションコードの活用 ( 1 ) slideToggleEx メソッドの作成

アニメコードの活用とは

これまで 9 回に亘ってエントリイしてきたアニメーションコードの解読を踏まえて、今度はアニメーションコードを活用した、実例を掲載してみようと思います。

作成した(及び作成予定の)実例
  1. slideToggleEx() プラグインメソッド

    こちらは苦労の末やっと作成を完了し、このエントリイの下の方にその実例とコードを掲載しました。

    このプラグインメソッドは、端的に言って slideToggle() メソッドの拡張版です。

    jquery.js に収められている slideToggle() は slideDown() と slideUp() メソッドを交互に実行する切り替えメソッドで、ボックス領域の表示と隠蔽を上辺を基点として切り替える作用を起こします。

    今回作成した slideToggleEx() メソッドは、slideToggle() が表示/隠蔽を上辺だけから行うのに対して、以下のように様々な箇所から表示され、隠蔽されるように、大幅に機能拡張したものです。

    slideToggleEx() メソッドは、ボックスの (0) 中心、(1) 左上、(2) 右上、(3) 右下、(4) 左下、(5) 上辺、(6) 右辺、(7) 下辺及び (8) 左辺の、全部で 9 つのアニメ起終点に対応させた、ボックスの縮小隠蔽 / 展開表示を行うアニメーションです。

    起終点は簡単に引数だけで指定出来るように工夫し、更に、同一ボタンのクリックによる、或る起終点からの隠蔽/表示アニメーションだけでなく、様々な起終点からのアニメを、連続して起動しながら、隠蔽/表示を繰り返す連続アニメーションにも対応させた、まさしく機能拡張版です。

  2. easing 関数の値をグラフ化するアニメーションを作成します。

    これはボックスのサイズを大きくするアニメーションを行なわせながら step メソッドを活用し、その時に使用している easing 関数の値を、当該ボックスの中にグラフとして示すものです。グラフをアニメーション機能を使って描くという試みにチャレンジしてみます。

    実は次のエントリイにそのグラフ描画アニメーションを掲載しました。

  3. ボックスだけでなく画像も同時に animate するメソッド

    こちらはまだ未着手です。cycle アニメーションのように、画像も一緒に拡大縮小するアニメーションを作る予定です。

    なおこのメソッドは、これまでのように表示されているコンテンツをアニメートするのではなく、非表示あるいは存在していなかったものをアニメートで出現させるタイプになるはずです。

  4. その他これから考えますが、余り複雑にしても意味はないでしょう。組み合わせれば済むことですから。

▲ToTop

完成した slideToggleEx() メソッドについて

何はともあれ、結果をお披露目します。

当然のことですが、 slideToggleEx() メソッドはプラグイン形式、つまり、jQuery プロトタイプオブジェクトとして作成しました。こうすれば使い勝手がより一層汎用的になるからです。

 
 
18 個のアニメーションを slideToggleEx() で一気に履行する

上の 「 slideToggleEx() 一気履行 」 ボタンをクリックすると、「 ボックスを終点に隠蔽し、そこを始点として展開する 18 の連続アニメーション 」 が始まります。

アニメーションは、ボックスの (0) 中心、(1) 左上、(2) 右上、(3) 右下、(4) 左下、(5) 上辺、(6) 右辺、(7) 下辺及び (8) 左辺の 9 つの点または辺をこの順番に起終点/線とし、 閉じて開く 2 回 × 9 種類、つまり 18 回連続して起動されます。このアニメではそれぞれのアニメ継続時間は全て 1 秒とし、easing 関数は全て同一の、穏やかな swing を使いました。

なお、この連続起動アニメも、上で説明する 3 つの個別起動アニメも全て、上の薄黄色地ボックスの中にある、 青地金色ボーダーのボックスを対象として作動します。また動くボックスの元の位置が分かるように、アニメ対象ボックスを包み込む不動の背景ボックス(背景色ブラウン)を配置してあります。これにより margin を含めてアニメ対象になっていることが視覚的にも確認できるはずです。

slideToggleEx() を単独で使ってみる。

(1) 起終点がボックス中心、(2) 起終線が左辺、(3) 起終点が右下隅 ── の 3 つを上に用意しました。

これらはそれぞれに単独で作動し、1 回目のボタンクリックで隠蔽アニメが、同一ボタンの2 回目のクリックで表示アニメが、それぞれ起動します。アニメ継続時間は全て 2 秒に設定しました。

easing 関数と IE

ここで easing 関数と IE の関係について一言付言しておきます。上の単独起動のアニメーションには全て IE で動くことを確認した、それぞれ異なる easing 関数を使用しています。

値が 0 を下回るか、1 を越えるような easing 関数を指定すると、Firefox では問題なく動くのですが、IE の場合固まってしまいます。使っている easing 関数セットは、最も有名と思われるオーソドックスな jQuery Easing Plugin (version 1.3) なので、関数指定に問題があるとは思えません。

どうして IE では動かないのか解明出来ず、このために上のサンプルでは、関数値が 0 ~ 1 に収まるような easing 関数を選んで指定しました。

▲ToTop

プラグイン slideToggleEx() の仕様

■ slideToggleEx() の仕様

 書式: slideToggleEx ( type, duration, easing, function )

 type: 0-中心、1-左上端、2-右上端、3-右下端、4-左下端、
     5-上辺、6-右辺、7-下辺、8-左辺
      ─── この数値でアニメ起終点位置を指定します。
 duration; アニメ継続時間(ミリ秒単位、または "slow","fast"。
       無指定の場合 jquery.js の仕様により 400 ミリ秒となります。)
       なお、オブジェクト形式でこの第 2 引数を指定することも可能です。
 easing: easing 関数(文字列で指定します。無指定の場合 
            jquery.js の仕様により swing が適用されます)
 function: アニメ終了後に起動する関数を指定します(不要ならば無指定)
 
 ※ 第 2 引数をオブジェクト形式で指定する場合には、第 3、第 4 引数は指定しません。

slideToggleEx() の仕様上の特徴

アニメ位置指定について

アニメの位置指定を絶対指定にするか、相対指定とするか、最初は悩みました。そしてその是非を判断するために、両方を作成した上で、ここには相対指定版を掲載しました。相対指定の方がコードをぱっと見て分かりやすいと判断したためです。

アニメの位置指定を絶対指定方式で行う場合には、アニメ対象をラップするボックスが位置指定の基準座標となるため、指定上の容易性を考慮するとアニメボックスのマージン辺と同じサイズの 「 基準ボックス 」 を作らねばなりません。

アニメ位置を相対位置で指定すれば、このような基準座標ボックスの必要性はないので、その意味からも相対指定方式の方が利便性がよいと思われます。

アニメ対象の position 指定について

当初はアニメ対象の CSS に position:relative を指定していましたが、slideToggleEx プラグインの運用を重ねるに連れ、その都度指定することが面倒であることに気がつきました。

slideToggleEx プラグインメソッドは top と left をアニメ対象としていますので、アニメ対象には static 以外の position 指定が為されていなければなりません。しかし、それを一々対象要素に指定するのは結構面倒です。

このため、仮にアニメ対象に position 指定が為されていない場合でも相対指定となるよう、Javascript コードによって強制的に指定することにしました。

outerWidth、outerHeight の true 指定について

jquery.js では outerWidth(true)、outerHeight(true) と指定すると margin 辺迄のボックスサイズを計測する仕様になっています。(#4342 ~ 4346)

ところが、非圧縮版の jquery.js においてはその通り機能するのですが、packed 版ではそのコードをチェックしても間違ってはいないと思われるのですが、巧く作動せずボーダー辺迄の採寸しかしてくれませんでした。

ここでは margin 辺までの外寸が欲しかったのでやむを得ず、outerWidth、outerHeight には引数 true を付けずに、ボーダー辺までのサイズを取得しておき、それに margin サイズを追加して margin 辺までの採寸を行わざるを得ませんでした。

▲ToTop

以下のコード等の表示/隠蔽ボタンには slideToggleEx を利用した

コードなどを表示したり隠蔽したりするボタンは、従来から slideToggle メソッドを仕込んでいました。

そして今回、当然ですが、そのボタンには slideToggle ではなく slideToggleEx を仕込みました。

早速実用に供したわけですが、使う上での注意点を列挙しておきます。

実用上の注意点
  1. 表示/隠蔽対象となる要素タグスクロールバーがある場合

    表示/隠蔽対象となる要素タグが、縦または横にスクロールバーがある場合、その要素を隠蔽後に表示させた場合、スクロールバーの幅や高さ分だけ、左にインデントが掛ってしまったり、下が空いてしまったりします。

    これは jquery.js における アニメ時の overflow プロパティの扱いに起因すると思われますが、確定的なことは調査し切れていません。

    従って、特に縦スクロールバーがある要素を slideToggleEx 対象とする場合には、ボックスの右の 2 つの端部や右辺を起終点/線にしないようにするか、あるいはボックス幅を明示的に指定することが賢明です。

  2. IE 対策

    IE ではボックスの右側を起終点とするアニメは巧く作動しません。スクリプトが固まってしまいます。

    このため右上端、右辺及び右下端の 3 つの起終点を指定した場合には、IE だけは、左上端、左辺及び左下端が指定されたものと読み替えるよう、コードを改変しました。

上のアニメ実例における slideToggleEx() の指定内容

直下のボタンをクリックすると、上のアニメ実例を起動させる指定内容が表示されますが、このボタンには No 5 の slideToggleEx 、つまり上辺からのスライドダウン/アップを仕込みました。不透明度もアニメ対象にしているので、slideToggle よりもより印象的になっている、と思います。なお、効果を確認するためにも幾つかのアニメは時間を 1 秒掛けるようにしました。

■上で示した実例の slideToggleEx() 指定内容
<script type="text/javascript">//<!--
$(function(){
 // 18 アニメ連続起動
 $.box = $("#box1_727");
 $("#btn1_727").click(function(){
  $box
   .slideToggleEx(0,1000,"swing")
   .slideToggleEx(0,1000,"swing")
   .slideToggleEx(1,1000,"swing")
   .slideToggleEx(1,1000,"swing")
   .slideToggleEx(2,1000,"swing")
   .slideToggleEx(2,1000,"swing")
   .slideToggleEx(3,1000,"swing")
   .slideToggleEx(3,1000,"swing")
   .slideToggleEx(4,1000,"swing")
   .slideToggleEx(4,1000,"swing")
   .slideToggleEx(5,1000,"swing")
   .slideToggleEx(5,1000,"swing")
   .slideToggleEx(6,1000,"swing")
   .slideToggleEx(6,1000,"swing")
   .slideToggleEx(7,1000,"swing")
   .slideToggleEx(7,1000,"swing")
   .slideToggleEx(8,1000,"swing")
   .slideToggleEx(8,1000,"swing",function(){alert("18 の連続アニメが完了しました")});
 });
 // 起終点をボックス中心とするアニメ起動
 $("#btn2_727").click(
  function(){
	 $box.slideToggleEx(0,1000,"easeInOutQuint");
	}
 );

 // 起終線をボックス左辺とするアニメ起動
 $("#btn3_727").click(
  function(){
	 $box.slideToggleEx(8,1000,"easeInOutSine");
	}
 );

 // 起終点をボックス右下端とするアニメ起動
 $("#btn4_727").click(
  function(){
	 $box.slideToggleEx(3,1000,"easeInOutQuart");
	}
 );

 // ボタン
 $("button")
  .mousedown(function(){$(this).css({backgroundColor:"lightgreen"})})
  .mouseup(function(){$(this).blur().css({backgroundColor:""})})
  .hover(function(){
    $(this).css({backgroundColor:"pink"});
   },function(){
    $(this).css({backgroundColor:""});
  })
  .blur();

// regist click Event to fire slideToggleEx() for nextBox
// button の次にあるブロックを slideToggleEx の操作対象とするための一般的コード
 	var duration=1000, easing=null,fn=function(){}, clk={};
	function func(i){
		$.each(clk[i],function(){
			var $target={};
			$target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
			if ($target[i].length) {
				$(this).click(function(){
					if (jQuery.browser.msie)
						var k = i==2 && 1 || i==3 && 4 || i==6 && 8 || i;
					else k = i;
					$target[i].slideToggleEx(k, duration, easing, fn);
					$(this).blur();
				});
			}
		});
	}
	for (var i=0; i<9; i++){
		clk[i] = $(".fireSlideToggleEx-" + i);
	  	if (!clk[i].length) continue;
		func(i);
		clk[i].hover(
			function(){$(this).css({backgroundColor:"pink"})},
			function(){$(this).css({backgroundColor:""})}
		)
		.mousedown(function(){$(this).css('background-color','palegreen')})
	}
});
//--></script>

プラグイン slideToggleEx() に係るスタイスシートと Javascript コード

直下のボタンには No 2 の slideToggleEx 、つまり右上端部からのスライドダウン/アップを仕込みました。最初は縦スクロールバーがないので、右上端部からの起終点も簡単に実行できると思いましたが、幅を明示的に指定して初めて思い通りに作動しました。

但し、またしても IE で障害が出ました。右上端部への隠蔽が起動せず、何故かスクロールバーが消えてしまうだけで他には何も起こりませんでした。仕方なく IE の場合のみ No1 つまり左上端部を起終点とするようにコーディングし直しました。

■ スタイルシート
<style type="text/css"><!--
 #animArea_727 {
  position :relative; z-index:1; margin:1em auto; top:0px;left:0px;font-size:16px;color:black;
  width:650px;height:300px;background-color:cornsilk;border:dotted 2px goldenrod;
 }
 #animArea_727 > div {
  position: absolute; z-index:2; top:50px; left: 170px;
  width:336px; height:186px; background-color:brown;
 }
 #box1_727 { /* 敢えて position 指定は行っていません。*/
  margin:10px 10px 10px 10px;width:300px; height:150px;
  background-color:royalblue; border:ridge 8px palegoldenrod;
 }
 button {overflow:visible;padding:2px;}
--></style>

▲ToTop

直下のボタンには No 7 の slideToggleEx 、つまり下辺からのスライドアップ( 表示する場合 )/ダウン( 隠蔽する場合 )を仕込みました。スライドが下から始まると独特の雰囲気になります。

■プラグイン slideToggleEx() メソッドコード
<script type="text/javascript">//<!--
// top と left をアニメ対象プロパティにする position relative バージョン
(function($){ // jQuery plugin : slideToggleEx() Release at 2009.8.21, verup 2009.9.9
var ver="not absolute but relative animation on position relative";
$.fn.slideToggleEx = function( type, duration, easing, fn ){
 var $target=this;
 var o = $.extend({},$.fn.slideToggleEx.opts);
 // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 if(!o.target || o.target[0]!==$target[0]) {
  $.extend(o,{
   orig : o, // backup しておく
   target : $target, // 起動インスタンスを記録する
   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
    + parseInt($target.css("margin-bottom")), // インスタンスの第一要素の margin 辺迄の高さを測る
   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
    + parseInt($target.css("margin-right")), // インスタンスの第一要素の margin 辺迄の幅を測る
   odd : false
  });
  // opts オブジェクトに上で得た o オブジェクトを併合し、Ex メソッドが終わっても情報を保存する
  $.extend($.fn.slideToggleEx.opts,o);
  // インスタンスの要素の position 指定を確認し、必要な指定を行う。
  if ($target.css("position") ==="static") $target.css({position:"relative",top:"0",left:"0"});
 }
 // 起動回数が偶数か奇数かを記録する。連続起動の際にはこれによって hide メソッドを起動するのか、
 // showメソッドを起動するのかを判断する。
 o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;
 o.hidden = $target.is(":hidden"); // 要素が隠れているかどうか判定する
 o.duration = duration || o.duration; // duration が オブジェクトの場合も含めて対応
 // 以下の 2 つは、duration がオブジェクトの時には空文字を代入する。
 o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";

 function makeAnimCSS(type,showhide){ // アニメCSSプロパティを作成する関数
  var type = new Number(type);
  var plmn = showhide ? "-=" : "+="; // showhide == true → "-="
  return type==0 ? {top: plmn + o.oH/2 +"px", left: plmn + o.oW/2 +"px"} : // center
  type==1 ? {top: plmn + 0, left: plmn + 0} : // 左上端
  type==2 ? {top: plmn + 0, left: plmn + o.oW +"px"} : // 右上端
  type==3 ? {top: plmn + o.oH +"px", left: plmn + o.oW +"px"} : // 右下端
  type==4 ? {top: plmn + o.oH +"px", left: plmn + 0} : // 左下端
  type==5 ? {top: plmn + 0, left: plmn + 0} : // 上辺
  type==6 ? {top: plmn + 0, left: plmn + o.oW +"px"} :// 右辺
  type==7 ? {top: plmn + o.oH +"px", left: plmn + 0} :// 下辺
  type==8 ? {top: plmn + 0, left:plmn + 0} : {top:plmn + 0, left:plmn + 0} ;// 左辺
 }

 if (new Number(type) > 8) // エラー対処
  (function(){
   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。\n"+"slideToggleEx( 0~8, 継続時間, easing関数 )"); return;
  })();
 else {
  var obj = (!o.odd || o.hidden) 
   ? makeAnimCSS(type,true)  // 偶数回目の起動か、または要素が隠蔽されている時の CSS 作成
   : makeAnimCSS(type,false); // 奇数回目の起動か、要素が表示されている時の CSS 作成
  $.extend( obj, // top と left だけのCSSオブジェクトに幅と高さ関係値も追加
    ( type==5 || type==7 ) ? {} : o.animWidth, // 上辺か下辺の時には幅方向CSSは対象外
    ( type==6 || type==8 ) ? {} : o.animHeight, // 右辺か左辺の時には高さ方向CSSは対象外
    {opacity:"toggle"} ); // 透明度もアニメ対象プロパティとする
  return this.animate(obj, o.duration, o.easing, o.complete); // アニメ起動
 }
}; // End of $.fn.slideToggleEx
// default 値設定と記憶用プロパティとしての機能のために
$.fn.slideToggleEx.opts = {
 duration:1000, easing:null, complete:function(){},
 animHeight : {
  height:"toggle", marginTop:"toggle", marginBottom:"toggle", paddigTop:"toggle",
  paddingBottom:"toggle", borderTopWidth:"toggle", borderBottomWidth:"toggle"
 }, animWidth : {
  width:"toggle", marginLeft:"toggle", marginRight:"toggle", paddigLeft:"toggle",
  paddingRight:"toggle", borderLeftWidth:"toggle", borderRightWidth:"toggle"
 }
};
})(jQuery);
//--></script>

▲ToTop

jquery.js におけるアニメーションコードの解読 ( 9 )

これまで 8 回に亘って jquery.js のアニメーションコードを解読してきました。自らの理解を深めるために、また閲覧者により関心を抱いて戴くためにも、いくつかのサンプルアニメーションも作成し、その解説もおこなってきました。

そしてついに、このエントリイをもって jquery.js のアニメーションコードの全ての解読を終えることになります。既に高い山は登り終えているので、残されたメソッドは比較的簡単なものばかりです。

jQuery().slideDown() メソッド

引数:speed、callback

返値:jQueryインスタンス

機能:要素の幅を一定に保ちつつ、高さを 0 から徐々に所定の高さまで変化させながら要素を表示します。

第 1 引数に高さに関わる各種の CSS プロパティ値を全て "show" に設定した CSS オブジェクトを、第 2 引数に speed を、第 3 引数に callback を設定して、animate メソッドを起動し、その返値を返します。

このメソッドは具体的には次のように定義されます。
 jQuery.fn.slideDown = function(speed,callback){
  return this.animate({
    height:"show", marginTop:"show", marginBottom:"show",
    paddingTop:"show", paddingBottom:"show"
  },speed,callback)
 } //(#3954~3964より)

slideDown メソッドのコード進行を具体的に跡付けると次のようになります。

  1. 高さに関わる各 CSS プロパティ値を全て "show" に設定した CSS オブジェクトを第 1 引数とすることにより、animate メソッドからそれらの各 CSS プロパティ毎に、当該 CSS プロパティ名称を引数として e.show(prop) メソッドが起動されます。(#3892-3896)
  2. 各プロパティ毎に起動される e.show(prop) メソッドは、プロパティが対象ノードのマージン上下、ボーダー上下、又はパッディング上下値の場合には初期値を 0 に設定し、それが要素高さの場合には 1 に設定します。また e.cur() メソッドを呼び出して、各々の要素属性値、styleオブジェクトのプロパティ値または算出スタイル値を終了値に設定します。こうして初期値と終了値を設定してから、各プロパティ毎に e.custom() メソッドを実行します。
  3. e.show() メソッドから起動された e.custom() メソッドは、e オブジェクトのプロパティに初期値、終了値、現在時刻などを格納してから、当該 CSS プロパティに対して、今度は e.step() メソッドを起動します。
  4. e.custom() メソッドから起動された e.step() メソッドは、その時迄の経過時間が duration 時間を下回っている場合には、指定された easing 関数があればそれを使い、なければ swing 関数を使って、jQuery.easing() メソッドを起動します。
  5. easing メソッドは、経過時間と継続時間を変数とする所定の easing 関数により、0 から 1 の間の数値を返す関数で、この値が得られたらば、次に easing 関数による値と、初期値と終了値の差などからその時点のプロパティ値を算出します。そしてこの当該 CSS プロパティの値を、e.now プロパティに代入します。
  6. こうして当該時点の当該プロパティの CSS 値を取得した後、e.update() メソッドを起動します。
  7. update メソッドは、当該要素に style オブジェクトがあって、かつ当該プロパティ値が null 値ではないときには、当該 style オブジェクトの当該プロパティに、e.now の値を単位付きで代入します。
    他方、当該要素に style オブジェクトがないか、それがあっても当該プロパティ値が null だったら、当該要素の当該属性に e.now の値を代入します。(#4020)
    その後、処理対象プロパティが height で当該要素に style オブジェクトが存在すれば、その display プロパティを block とし、style オブジェクトがなければ何もしません。
  8. こうして高さに係る style オブジェクトの各 CSS 値または要素属性に e.now を与えられた当該要素は、ブラウザによりその e.now 値に従って画面に表示されます。
  9. 以上の 3. ~ 7. までの、一部にクラスメソッドを含む、e インスタンスオブジェクトに属するメソッドの連続実行( e.show → e.custom → e.step → $.easing → e.update ) により、当該要素の当該プロパティの、その時の値を style オブジェクトか要素のタグ属性に代入してから、e.show メソッド実行過程の最後において、show() インスタンスメソッドが引数なしで起動されます。(#4080)
  10. しかし、既に 7. において 要素属性に e.now 値が代入されるか、elem.style.display 値が "block" となっているので、この時点で表示行為は終わっています。
  11. では、何故引数なしで show() インスタンスメソッドを起動する必要があるのでしょうか。
    この疑問を質すにはこのメソッドが、jQuery.data() クラスメソッドを使用して、インスタンスの各要素に elem.style.display プロパティ値を関連づけることを思い出す必要があります。つまり、このメソッドは単に画面に要素を表示するだけではなく、その要素が直前に表示済みか、隠蔽されているかを記録する機能を有しています。ここで show() インスタンスメソッドを起動する目的は、この「履歴情報」を設定し、あるいは確認する事にあります。
  12. 具体的に言えば、ここで起動される show() メソッドにより、display 値が block であることが当該要素への紐付け行為( $.data() メソッドの実行)によって「登録され」、あるいは登録済みであることが確認されます。

▲ToTop

jQuery().sideUp()

引数:speed、callback

返値:jQueryインスタンス

機能:要素の幅を一定に保ちつつ、高さを所定の高さから 0 まで変化させながら要素を隠蔽します。

このメソッドは具体的には次のように定義されます。
 jQuery.fn.slideUp = function(speed,callback){
  return this.animate({
    height:"hide", marginTop:"hide", marginBottom:"hide",
    paddingTop:"hide", paddingBottom:"hide"  },speed,callback)
 } //(#3954~3964より)

このメソッドは、高さに関わる各種の CSS プロパティ値を全て "hide" に設定したCSSオブジェクトを第1引数に、speed を第2引数に、そして callback を第3引数にして animate メソッドを起動し、その返値を返します。言うまでもなく、jQuery().slideDown() メソッドの逆のことを行うメソッドです。

e.hide() を起動することにより、高さに係る各種 CSS スタイルプロパティのカレントスタイル値を初期値をとし、最終値をゼロに設定してアニメーションを行います。

具体的なコード進行は slideDown と酷似しますが、e.show()メソッドよりも、e.hide()メソッドの方が、また show()インスタンスメソッドよりも hide() インスタンスメソッドの方が、共にコード文字数は少なく、単なる対照的な関係ではありません。

  1. 高さに関わる各 CSS プロパティ値を全て "hide" に設定したCSSオブジェクトを第1引数とすることにより、animate メソッドからそれらの各 CSS プロパティ毎に、CSS プロパティ名称を引数として e.hide(prop) メソッドが起動されます。(#3892-3896)
  2. 各プロパティ毎に起動される e.hide(prop) メソッドは、当該要素の style オブジェクトの当該プロパティ値を this.options.orig[this.prop] に代入してから、高さに係る当該 CSS プロパティのカレントスタイル値を初期値に設定し、終了値をゼロに設定して、各プロパティ毎に e.custom() メソッドを実行します。
  3. e.hide() メソッド内で起動された e.custom() メソッドは、アニメーションの各種条件を設定するためのものです。
  4. e オブジェクトに初期値や終了値、現在時刻などをプロパティとして格納してから、当該 CSS プロパティに対して、今度は e.step() メソッドを起動します。
  5. e.custom() メソッドから起動された e.step() メソッドは、custom メソッドで設定された諸条件値を使ってアニメーションを「刻みます」。つまり、時間経過と共に変動する CSS 値を経過時間に即して設定します。
  6. その時迄の経過時間が指定された duration 時間を下回っている場合には、指定された easing 関数があればそれを使って、なければ swing 関数を使って、jQuery.easing() メソッドを起動します。
  7. easing メソッドは、経過時間と継続時間を変数とする所定の easing 関数により、0 から 1 の間の数値を返す関数で、この値と初期値と終了値の差などから、その時点のプロパティ値を算出します。そしてこの当該 CSS プロパティの値を、e.now プロパティに代入します。
  8. こうして当該時点の当該プロパティのCSS値を取得してから、表示を制御する e.update() メソッドを起動します。
  9. update メソッドは、まさに表示の「更新」を行うものです。
  10. 当該要素に style オブジェクトがあって、かつ当該プロパティが null 値ではないときには、当該 style オブジェクトの当該プロパティに、e.now の値を単位付きで代入します。他方、当該要素に style オブジェクトがないか、それがあっても当該プロパティが null だったら、当該要素の当該属性値に e.now の値を代入します。その後、処理対象プロパティが width か height だったら、当該要素の style オブジェクトの display プロパティを block とします。block により、幅や高さは初期値から次第にゼロに近づく e.now 値に応じて、表示サイズが小さくなっていき、最終的にはゼロに到達して当該要素は全く表示されなくなります。
  11. 3 ~ 7 までの、一部にクラスメソッドを含む e オブジェクトに属する一連のメソッド連続実行( e.show → e.custom → e.step → $.easing → e.update )により、当該要素の当該プロパティの、その時の値をstyleオブジェクトか要素のタグ属性に代入するわけです。

▲ToTop

jQuery().slideToggle()

引数:speed、callback

返値:jQueryインスタンス

機能:e.show() 又は e.hide() メソッドを交互に起動するメソッドです。対象としたタグ要素が表示されていれば e.hide()メソッドを起動して隠蔽し、隠蔽されていれば e.show() メソッドを起動して表示します。

jQuery().fadeIn()

引数:speed、callback

返値:jQueryインスタンス

機能:CSS オブジェクト {opacity:"show"} を第1引数に、speed を第2引数に、callback を第3引数にして animate メソッドを起動し、その返値を返します。これにより要素が透明から次第に不透明になっていって、表示されるアニメが実現されます。

このメソッドは具体的には次のように定義されます。
 jQuery.fn.fadeIn = function(speed,callback){
  return this.animate({opacity:"show"},speed,callback)
 }
  1. 第1引数により、animate メソッド内から e.show()メソッドが起動され、opacityスタイル値の初期値を 0 に、終了値を現在値に設定して、e.custom メソッドを作動させます。
  2. e.custom メソッド→ e.step メソッド→ e.update メソッドの連鎖起動を通じて、不透明度アニメーションの初期値などの設定とその時点における値設定を行ってから、e.update メソッド内から e インスタンスを引数にして、jQuery.fx.step[opacity] クラスプロパティメソッドが起動されます。
  3. jQuery.fx.step.opacity メソッドにより、当該要素の不透明度が step メソッド内で設定された e.now 値に設定されます。

jQuery().fadeOut()

引数:speed、callback

返値:jQueryインスタンス

機能:fadeIn メソッドとの違いは animate メソッド第 1 引数の opacity プロパティ値が hide になることだけです。これにより要素が不透明から次第に透明化していき最後には完全透明になって、表示が消えます。

このメソッドは具体的には次のように定義されます。
 jQuery.fn.fadeIn = function(speed,callback){
  return this.animate({opacity:"hide"},speed,callback)
 }

▲ToTop

jQuery().stop(clearQueue,gotoEnd) メソッド

引数:2 つの引数は共に boolean 型。最初の引数を true に設定すると、アニメ待ち行列配列を空にすることによって、次以降に登録されているアニメを全て登録から削除します。2 番目の引数を true に指定すると、全てのアニメーションの全ての step メソッドを格納している jQuey.timers 配列から、今まさにアニメを停止しようとしているタグ要素に関わる step メソッドを特定し、当該要素の当該アニメを、その step メソッドを最後に終了させます。

返値:jQuery インスタンス

機能:このメソッドは登録されているアニメーションを削除したり、進行中のアニメーションを停止させます。

引数に何も指定しないと、単に現在動いているアニメーションを停止させます。同じ要素に登録されている次のアニメーションは停止されませんし、停止指示の前の時点で動き出していて未だ終わっていない e.step メソッドも停止されません。

3927: stop: function(clearQueue, gotoEnd){
3928:  var timers = jQuery.timers;
3929:
3930:  if (clearQueue) // clearQueue が true ならば
3931:   this.queue([]); // 空配列を queue メソッドの引数として、待ち行列を空にし
3932:   // jQuery インスタンスに登録されているアニメーションを削除する。
3933:  this.each(function(){ // インスタンスの要素毎に巡回処理
3934:   // go in reverse order so anything added to the queue during the loop is ignored
3935:   for ( var i = timers.length - 1; i >= 0; i-- )
       // timers 配列の要素は custom メソッド内の #4046 行で定義されている
       // t 関数であり、これは e.step メソッドを起動させアニメを刻む役割を果たす。
       // この関数には animate メソッドが起動された全てのタグ要素の、全てのアニメーションの
       // 各 CSS プロパティ毎の e.step メソッドが登録されている。
       // それを逆順に巡回走査して、それが現在対象としている要素のメソッドだったら
3936:    if ( timers[i].elem == this ) {
3937:     if (gotoEnd) // gotoEnd が true に指定されていれば
3938:      // force the next step to be the last
3939:      timers[i](true); // 次に起動する e.step メソッドを最後にそのアニメを終わる。
3940:     timers.splice(i, 1); // timers 配列の i 番目を削除。実行中の e.step メソッドを削除する。
3941:    }
3942:  }); // 巡回処理終わり
3943:
3944:  // start the next in the queue if the last step wasn't forced
     // gotoEnd が 未定義か false ならば当該要素に登録されている次のアニメ関数を実行する。
3945:  if (!gotoEnd)
3946:   this.dequeue();
3947:
3948:  return this;
3949: }

jQuery.easing() クラスメソッド

引数:easing 関数名を文字列形式で指定します

返値:easing 関数による産出値

機能:初期値、アニメ継続時間、経過時間などから計算が行われますが、単純に言えば、様々な代数式に、経過時間割合( この割合のことを「 動作ステータス 」というらしい )を変数として代入して値が算出されます。

留意すべき点は、経過した時間そのものではなく、そのアニメ継続時間に対するその割合が変数となることでしょうか。

なお以前にも紹介しましたが、 easing 関数の極めて分かりやすい例示として、以下のサイトが非常に参考になります。「その Easing の各々の動きの違いを示したサイトとして興味深いのは、flash ムービーですが、 easing_demo が実に分かりやすく有益です。これによりそれぞれの Easing がどんな挙動をするのか一目瞭然に理解できます。easing 関数内の代数式がグラフ化されていることも非常に有益です。」( jQuery プラグインとしてアニメーションポップアップ : animatedPopup を自作した。 )

jquery.js におけるアニメーションコードの解読 ( 8 )

アニメーションの進行途中の情報を見るサンプル

このエントリイでは前エントリイに続いてサンプルを掲載します。今回のサンプルでは Web サイトでは余り見かけない「 アニメ-ションが進行しているその途中の情報 」を取り上げます。

これまでの 「 jquery.js におけるアニメーションコードの解読 (1) ~ (6) 」 からも分かるとおり、アニメーションが実現されるコードの進行過程は大変複雑で、様々なプロパティとメソッドが駆使されて 「 動き 」 が演出されています。

そしてアニメを扱ったサイトの多くは、アニメそのもの多様性・独自性・ユニークさを競い合っていても、アニメ-ション進行過程における情報を可視化したものは余り見受けません。

そこで、実際にアニメーションを起こしながら、その進行過程における諸情報を取りだすサンプルを作ってみました。

アニメ途中の諸情報とは何か

それはずばり jQuery.fx コンストラクタの e インスタンスが保持している情報です。

e インスタンスは既に見たように模式図的に示せば以下のような盛りだくさんの情報を内包しています。

■ jQuery.fx コンストラクタの e インスタンス
 e = {
 elem:elem,// 第 1 引数
 prop:propName, // 第 3 引数
 options: {
  queue:false || true, step:function(){・・・}, // これらはユーザーが指定する。
  old,complete,easing,duration,display,overflow,// animateメソッドから引き継がれる。
  curAnim, //animateメソッドの第一引数である prop オブジェクトが格納される。
  orig:{prop},
  show:true || false || undefined, hide:true || false || undefined
 },
 startTime, start, end, unit, now, pos, state,
 // メソッド
 cur(), custuom(), step(), update(), show(), hide()
}

■ jQuery.fx コンストラクタのクラスプロパティ
 jQuery.fx.speeds = {"slow":600, "fast":200, _default:400}
 jQuery.fx.step = {opacity, _default}

▲ToTop

「 アニメ進行過程 」における当該アニメの諸情報は、e.step メソッドがアニメ対象要素( 以下 A )のアニメ対象プロパティ( 以下 props )毎に、ミリ秒単位で静止画を繰り返し描画する過程において、刻々と変化します。

諸情報は e インスタンスオブジェクトのプロパティ【 startTime, start, end, unit, now, pos, state 】に、何百回となく上書きが繰り返されます。

このエントリイではこれらの値を取りだしてみます。

もちろんその全てを取りだしていたら数千,数万のデータとなるため、ここではアニメ継続時間 ( duration )の 15%、30%、50%、75%及び 100%の時間が経過した 5 つの時点に絞って、それぞれの経過時点におけるプロパティ値を取り出すこととします。

なお、折角閲覧して戴くので、閲覧者が操作にある程度関われるよう配慮しました。

第 1 に props(アニメ対象プロパティ)を選択できるように、第 2 に、アニメで使用する easing 関数を多数用意し、これも閲覧者が選択できるようにしました。

アニメ途中経過情報を表示するサンプル

以下がサンプルです。ボックスが 6 つ。左から順にアニメ素材としたボックス、アニメ開始後の時間が、設定済みの継続時間の 15 %進行した段階で止まるボックス、以下 30%、50%、75%でそれぞれ停止するボックス、最後が通常通りアニメーションされるボックスです。なお、アニメ継続時間は、進行過程をゆっくり見せるために一律に 4 秒としました。

4 つの進行時間が異なるアニメの諸情報 ( 各 A の props。具体的には開始時刻, 開始値, 終了値, 経過割合, 値の変化度合, 停止値, 経過時間 ) は下の方に配置した 4 つの表内に、アニメが停止したその瞬間に描画されます。

アニメ
素材
15 %
30 %
50 %
75 %
100%

適用 easing 選択

アニメ継続時間の選択 

アニメ対象プロパティの選択

  
  

▲ToTop

途中停止アニメサンプルの使い方
  1. まず、easing 関数、アニメ継続時間(所要時間と言っても良い)およびアニメーションの対象プロパティを選択します。
  2. 「 Do 隠蔽アニメ 」 ボタンをクリックします。

    これで隠蔽アニメーションが始まり、各々のボックスアニメが指定した途中の段階で停止します。同時にその瞬間に、停止時のアニメに係る諸情報が下の表に出力されます。

    この情報によって jquery.js が処理しているアニメ情報の具体的な内容を知ることが出来ます。

  3. 「 最後まで動かす 」 ボタンをクリックします。これで途中で停止していたアニメが再開されて最後まで進行します。最後まで進行するとアニメ素材以外のボックスは全て隠蔽された状態になります。

    なお、アニメ再開時の諸情報は表には出力されません。

  4. 「 Do 表示アニメ 」 ボタンをクリックします。これによって今度は表示アニメーションが始まります。ここでも途中で停止し、その時の情報が表に上書き出力されます。
  5. 最後に再びアニメを再開させます。「 最後まで動かす 」 ボタンをクリックすれば、表示アニメーションが最後まで進行して、一連の 「 停止─再開 」サンプルアニメーションが終了します。 この段階で初期の 6 つのボックス表示状態に戻ります。

言うまでもなく、easing 関数を変え、継続時間を変え、あるいはアニメ対象プロパティを変えて、何回でも途中停止アニメーションサンプルを起動させることが出来ます。

なお、ボタン、セレクトボックス及びチェックボックスは、アニメーション進行中や途中停止状態の時には使えないように disabled 属性を調整すると共に、カバーボックスをアニメートさせ、使用可能/不能が視覚的にも一目瞭然となるように工夫しました。

所定の継続時間(4000ミリ秒に設定)の 15 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 30 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 50 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 75 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize

▲ToTop

アニメ途中経過情報を表示するサンプルの要点

アニメ-ション進行過程のあらまし(再掲)

jquery.js の animate メソッドはアニメーション動作開始前に e インスタンスを作成し、そのプロパティ上にアニメーションに係る多数の情報を保持し続け、上書きを繰り返してアニメを実現しています。そのことはアニメーションに関するシリーズエントリイの最初( jquery.js におけるアニメーションコードの解読 ( 1 ) )でも触れました。

その e インスタンスには、数種類のメソッド ( cur, custuom, step, update, show, hide ) も形成され、これらのメソッドが有機的に連携して 「 動き 」 を演出します。animate メソッド内でのこれらのメソッド連携を改めて跡付けてみると次のようになっています。

  1. 最初に animate メソッドの第 1 引数内で指定されたアニメーション終了値を処理します。

    その指定値が show、hide または toggle の時には e.show 又は e.hide メソッドが起動されて、その中で必要な処理を行ってから、e.custum メソッドが呼び出されます。

    指定された終了値がそれらの文字列ではない場合には、その値をanimateメソッド内で評価処理してから( この時に現在値を計算するメソッドが e.cur です )、やはり、e.custum メソッドが呼び出されます。

    こうして animate メソッド内の e インスタンスサブルーチンメソッド連鎖は必ず e.custom メソッドから始まります。

  2. e.custum メソッド内ではアニメ開始時刻を記録し、アニメーションの開始値を取得した後に、e.step メソッドを呼び出します。
  3. e.step は呼び出された時刻を記録し、その時点におけるアニメプロパティの現在値を算出します。この時に現在値を計算するメソッドが e.cur です。
  4. その時点の現在値が決まれば後はブラウザ上に描くだけです。それを e.update メソッドが行います。
  5. この過程が何百回、何千回となく繰り返されて 「 動き 」 が描かれます。
アニメ途中で何かを実行させるメソッド: e.options.step

さて、アニメーション実行中にユーザーが出来ることは、ブラウザを停止する、パソコンの電源を切る等の外的作用の他には、(1) アニメが終わるのを待つか、(2) e.options.step メソッドをあらかじめ指定しておき、アニメーション進行途中でそれを実行させるか、この 2 択 しかありません。アニメを途中で停止させる指示を与える場合も、e.options.step メソッド内に $().stop メソッドをあらかじめ仕込んでおくことになります。

ここでは (2) の途中で何かをさせるメソッド e.options.step の指定方法について言及します。

e.options.step の指定について(jquery.js #4018 参照)

このメソッドは e.update メソッドの冒頭で起動されます。アニメ対象プロパティ毎に、数ミリ秒毎に繰り返し実行される e インスタンスメソッドチェーン内において、何百回、何千回と描画が繰り返されますが、その描画直前毎に、e.options.step メソッドが実行されます。

この e.options.step の引数は、アニメプロパティ現在値と e インスタンスの 2 つで、起動元の this.elem と併せて jquery.js #4018 で無条件に指定されます。起動元はアニメーション対象要素タグに指定されますから、e.options.step メソッド内において this キーワードを使った場合、その参照先は対象要素タグとなります。

また、 引数に e インスタンスがあると言うことはアニメに係る全ての情報に、 e.options.step メソッドからアクセスできることを意味しています。この点は極めて重要なポイントです。

以上の実行タイミングと引数を踏まえた約 200 行のコードによって、このエントリイのサンプルを実現しました。具体的な方法は、後に掲載したコードの通りですが、要点を記しておきます。

サンプルコードの要点

ユーザーフレンドリイに!

これまでは簡単に言えば「 面倒なので 」(苦笑)、form 要素の使用を出来るだけ避けてきました。せいぜいボタンを単独で多用してきた程度で、リストボックス、コンボボックス、チェックボックスなどのブラウザ閲覧者が操作し、その結果を反映させられるツールは殆ど使ってきませんでした。

その意味では今回のチャレンジは、step メソッドの使い勝手を試してみる以外に、form 要素を駆使するコーディングに真正面から取り組んだ点が大きな特徴であり、またそれがハードルともなりました。

ここで配置した form 要素の使いやすさについては閲覧者の批判を甘受するしかありませんが、アニメ中に form 要素を変更されなくするための、思いつきで作った二重の処理( disabled 属性の処理と隠蔽カバーをアニメで被せたこと)には、それなりに満足しています。

なお、このカバーアニメーションにおいてもユーザー指定の easing 関数を使うように設計してあります。

途中停止のための動作ステータスキャッチ

options.step メソッドにおけるアニメの途中停止は、動作ステータス ( e.state ) の操作により行いました。

プロパティ値の変動割合で行うことも可能ですが、この値は easing 関数によって増減する動的なものなので、時間経過によって途中の段階を捉える方が確実だと考えました。

e.state は 或る e.step メソッドの 「 呼び出し時迄の経過時間÷アニメ継続時間 」で定義されている、「 経過時間割合 」とでもよぶべきものですが、この値は決して数直線のような連続的な数値にはなりません。

当該要素の当該プロパティ毎に、繰り返し呼び出される e.step の呼び出し時迄の経過時間は必ず一定間隔が空くからです。ちょうど 15 %過ぎた、そのタイミングの時に、偶々 e.step が呼び出されているという保証は全くありません。

このため 15 %前後に一定の幅を持たせた 「 許容域 」を設定しないと、アニメ停止のタイミングが捉えられません。このため、if ( e.state > (( stopPoint - permitter )/100 ) && e.state < (( stopPoint + permitter )/100) ) と言う式によって、一定の許容域に収まるタイミングを捉えて、その瞬間にアニメを停止させるようにしました。stopPoint は 15 %等の値、permitter は許容幅です。

諸情報の表における s 列がその許容域内で補足されたことを示す経過時間割合です。

しかも、許容幅はアニメ継続時間との関係で変動させる必要がありました。余り短時間の継続時間だと e.step の起動回数が少なくなるので、許容幅を広げないと「 その瞬間 」を捉えられないのです。こうして継続時間が長ければ許容幅は小さく、継続時間が短い場合には長くなるよう、動的に変動するようにしました。

4 つの機能を持たせたアニメ起動ボタン

出来るだけ簡単な操作になるよう、隠蔽アニメ起動、隠蔽アニメ停止後の再開、表示アニメ起動、表示アニメ停止後の再開───という一連の操作を全て 1 つのボタンだけで行うようにしました。

その結果コードは複雑にならざるを得ませんでした。4 回のそれぞれの操作の意味が全て異なるため、それぞれ毎に異なるコーディング他必要となるためです。これを click イベントの Toggle メソッド利用と、その中の 2 つの関数の起動回数(奇数回か偶数回か)によって 4 つに分けるようにしました。

IE 対策

今回もまた IE 対策を施さねばなりませんでした。W 3 C 準拠になったはずの IE 8 においてもまだ、準拠が不十分な点が残っているのでしょう。

具体的にはアニメーション対象プロパティとしてフォントサイズを指定した場合の対応です。Firefox の場合には全く問題がなかったのですが、フォントサイズをアニメ対象とした場合において、IE においては初期値のフォントサイズを指定しないと、アニメ前のフォントサイズを勝手に最大サイズ( おそらく96 pt )と解釈してしまうのです。

仕方なく、アニメ領域だけフォントサイズを指定せざるを得ませんでした。

Web サイト作成上、フォントサイズを固定的に指定することは推奨されていません。ユーザーがブラウザの拡大/縮小機能を使った場合に、フォントサイズが固定されているとフォントだけ拡大/縮小されないからです。ですから私の場合フォントサイズ指定は全く行ってこなかったのですが、今回の場合やむを得ず指定せざるを得ませんでした。

アニメ停止後再開時点における、開始値と終了値の操作に最も苦労しました

隠蔽アニメーションは開始時点のプロパティ値( 開始値 )を e.hide メソッド内で取得し、表示アニメは終了時点のプロパティ値( 終了値 )を e.show メソッド内で取得します。そして e.hide メソッドはアニメ対象要素の属性値や style 値を一切変化させません。だからこそ、e.show メソッドの終了値は、その直前に行われた e.hide メソッドの開始値に等しくなります。

ここに、開始値も終了値も共に、e.cur メソッドによって、アニメ対象タグの (1) 属性値や (2) style オブジェクト値、あるいは (3) 算出スタイル値から算出されます。

さて、停止後に再開されるアニメの初期値は、停止時点のプロパティ値となります。そして、停止時のプロパティ値は、既に動いた後なのですから、最初にアニメを起動した時のプロパティ値とは決して同一ではありません。

従って、アニメ対象プロパティを何も変更せず、単純に停止していたアニメを再開させると、再開後の最終値は当初の設定値と異なってしまいます。

例えばここのサンプルで言えば、15%で停止させたアニメを再開して隠蔽させ、その後最後まで表示させると、その表示サイズは 15% で停止した時のサイズであって、決して当初の大きさまでは戻りません。

この問題をどうやって解決するか───この点こそ、今回のコーディングで最も頭を悩ませた箇所でした。

一旦 animate メソッドが起動してしまうと、その途中でアニメ対象プロパティをユーザーが直接変更することは、options.step メソッドを使ってアニメを進行途中で止めたとしても困難です。

何故ならば、アニメ対象プロパティは、既に animate メソッドによって作成済みの e インスタンスオブジェクト内の各プロパティに値が格納されてしまっていて、コードによって e インスタンスを操作することは出来ないからです。

出来ることは、e インスタンスのメソッドが作動する際に、style オブジェクト、要素属性又は算出スタイルから、各プロパティ値を算出することに着目して、要素の style オブジェクトの各プロパティ値や、定義できる属性であれば要素属性値を、次の animate メソッドが始まる前に、強制的に書き換えてしまい、それによって次の animate メソッドの開始値や最終値を前もって変更してしまうことだけです。

このことに気がついた結果、途中停止後に再開したアニメーションも、最初のアニメーションの開始値と終了値を参照するように強制的なコーディングを施して、やっとこのサンプルが完成したのでした。

▲ToTop

 #animArea_725 {
  position: relative; width:620px; margin:1em auto; height:140px;
  background-color:lavender;border:0;font-size:16px;
 }
 #box0_725, #box15_725, #box30_725, #box50_725, #box75_725, #box100_725 {
  position:absolute; top:10px; width:64px; height:84px; padding:3px;margin:5px;
  background-color:darkgreen; border:8px ridge bisque;
 }
 #box0_725 {
  left:10px;
 }
 #box15_725 {
  left:110px;
 }
 #box30_725 {
  left:210px;
 }
 #box50_725 {
  left:310px;
 }
 #box75_725 {
  left:410px;
 }
 #box100_725 {
  left:510px;
 }
 #formBlock {
  position:relative; width:600px;height:220px;margin:1em auto; border:1px dotted aquamarine;
  line-height:1.1em; clear:both;
 }
 #fm_725 {
  position:absolute; z-index:2; width:170px; margin:1em;
 }
 #fm_725 p{
  color:black;background:ivory;text-indent:-0.5em;text-align:center;line-height:1.2em;
  width:170px;
 }
 #fm2_725 {
  position:absolute;left:190px;margin:1em;
  width:345px; padding:10px;color:black;background:ivory;
  height:175px;
 }
 #submit_725 {margin:0.5em 1em 1em; padding:5px;border:2px solid royalblue;text-align:center;}
 #fm1shutter_725 {
  position:relative; z-index:2; top:-176px;
  width:170px; height:178px; background:teal; display:none;
 }
 #fm2shutter1_725 {
  position:absolute; z-index:2; left:6px;top:8px;
  width:350px; height:130px; background:teal; display:none;
 }
 #fm2shutter2_725 {
  position:absolute; z-index:2; left:6px;top:142px;
  width:350px; height:50px; background:teal; display:none;
 }
 input,button {overflow:visible; padding:2px;}
■ Javascript codes
(function(){
 var $fm2 = $("#fm2_725"), $chk = $(".chkbox"), $done = $("#go"),$initAnim = $("#initAnim_725"),
  $box0=$("#box0_725"), $box1=$("#box15_725"), $box2=$("#box30_725"),
  $box3=$("#box50_725"), $box4=$("#box75_725"), $box5=$("#box100_725"),
  $t1=$("#table1_725 tr"), $t2=$("#table2_725 tr"),
  $t3=$("#table3_725 tr"), $t4=$("#table4_725 tr"),
  $shutter1=$("#fm1shutter_725"), $shutter2=$("#fm2shutter1_725"), $shutter3=$("#fm2shutter2_725"),
  selEasing="swing", // 選択された easing 関数を入れる変数。既定値はswingとしておく
  chkList=[], // 選択されたチェックボックスリスト
  animProp=[], animaCSS, boxCSS={},initCSS=[], dur=4000, //既定値4秒;
  permitter=0.9, // パーセント許容幅(既定値0.9)
  odd = [false,false] , // 第 1 関数及び第 2 関数の起動回数が奇数かどうかを odd 配列に記録
  ival, obj={},
  fxAttrs = [
   ["marginTop","marginRight","marginBottom","marginLeft"],
   ["borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth"],["paddingTop","paddingRight","paddingBottom","paddingLeft"]
  ], 
  attrs=[];

 // 全プロパティ名の単純なリスト配列作成
 for (var i=0; i<fxAttrs.length; i++)
  for (var j=0; j<fxAttrs[i].length; j++)
   attrs.push(fxAttrs[i][j]);
 attrs = attrs.concat("width","height","opacity","fontSize");

 // easing リストから 選択された easing 関数を知る
 $("#sel_725").change(function(){
  selEasing = $(this).val();
 });

 // アニメ継続時間コンボボックスから 選択された duration 値を dur 変数に代入する
 $("#fm2_725 select").change(function(){
  dur = +$(this).val();
  permitter = (dur >2000) ? 0.9 : (dur > 800) ? 3 : 4;// 許容値を継続時間毎に変化させる
 });

 // チェックボックスの既定値選択設定
 $("#defaultSelect_725").click(function(){
  $("#fm2_725 input:checkbox").val(
   ["margin","padding","width","height","opacity"]
  );
 });
 // チェックボックス全選択設定
 $("#allSelect_725").click(function(){
  $("#fm2_725 input:checkbox").val(
   ["margin","border","padding","width","height","opacity","fontSize"]
  );
 });

 // アニメ開始前の全てのCSSプロパティ値を initCSS 配列に記録する
 $.each([$box1,$box2,$box3,$box4],function(){
  for (var j=0; j<attrs.length; j++)
   boxCSS[ attrs[j] ] = this.css( attrs[j] );
  initCSS.push(boxCSS);
 });

 // アニメ用のCSSオブジェクト( margin、borderWidth、padding )を作成するmeta関数
 var makeCSS =function(i,type){
  $.each( fxAttrs[i],function(){obj[this]=type;} ); return obj;
 }

 // チェックボックス情報を踏まえたアニメ用の CSS オブジェクト作成
 var make_animObj = function(type){
  var retObj={}, mStr = chkList.join();
  if (mStr.match("margin")) retObj = $.extend(retObj,makeCSS(0,type));
  if (mStr.match("border")) retObj = $.extend(retObj,makeCSS(1,type));
  if (mStr.match("paddin")) retObj = $.extend(retObj,makeCSS(2,type));
  if (mStr.match("width")) retObj = $.extend(retObj,{width:type});
  if (mStr.match("height")) retObj = $.extend(retObj,{height:type});
  if (mStr.match("opacity")) retObj = $.extend(retObj,{opacity:type});
  if (mStr.match("fontSize")) retObj = $.extend(retObj,{fontSize:type});
  for (var p in retObj) animProp.push(p); // アニメ対象プロパティリストを animProp 配列に格納
  return retObj;
 }

 // 一時停止時の情報を table に書き込む
 function printOut(o,box){
  var startTime =o.startTime.toString();
  var output=[
   startTime.substring(startTime.length-5,startTime.length), o.start, 
   Math.round(o.end*100)/100,
   Math.round(o.state*10000)/100+"%", Math.round(o.pos*10000)/100+"%",
   Math.round(o.now*10000)/10000, parseInt(dur * o.state)
  ];
  for (var i=0;i<16;i++){
   if (attrs[i]==o.prop ){
    for (var j=0; j<7; j++)
     box===$box1 ? $t1.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box2 ? $t2.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box3 ? $t3.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box4 ? $t4.eq(i+1).children().eq(j+1).text(output[j]) : null;
   }
  }
 }
 // goAnim メソッドから呼び出されて一時停止時の情報を取得する
 function doStep(box,obj,stopPoint){
  box.animate(obj,{
   step:function(){
    if (stopPoint==100) return;
    var e = arguments[1];
    if (e.state > ((stopPoint-permitter)/100) && e.state < ((stopPoint+permitter)/100)){
     var o = $.extend({},e);
     if (o.prop === animProp[animProp.length-1]) {
      box.stop();
     }
     printOut.call(null,o,box);
    }
   },
   duration:dur, easing:selEasing
  });
 }
 // type="toggle" で show/hide を循環させる
 function goAnima(type){
  var stopTime =[];
  stopTime =[15,30,50,75,100];
  animaCSS = make_animObj(type);
  $.each([$box1,$box2,$box3,$box4,$box5],function(i){
   doStep(this,animaCSS,stopTime[i]);
  });
 }

 // アニメと対象画像の初期化
 $initAnim.click(function(){
  $.each([$box1,$box2,$box3,$box4],function(i){
   this.stop(true,true);
   for (var j=0; j<attrs.length; j++){
    $.attr(this.get(0).style, attrs[j] ,initCSS[i][attrs[j]]);
   }
  });
  $(this).blur();
 });

  // select オプション・チェックボックス・コンボボックスの使用不能/可能を切り替える
 function elementsDisabled(type){
  $("#fm_725 option").attr("disabled",type).get(0).disabled="disabled"; // easingリストボックス
  $("#fm2_725 option").attr("disabled",type); // duration コンボボックス
  $("#fm2_725 input:checkbox").attr("disabled",type); // プロプ選択ボックス
  $("#defaultSelect_725").attr("disabled",type); // 既定選択ボタン
  $("#allSelect_725").attr("disabled",type); // 全選択ボタン
 }

  // チェックボックスのオンオフ状態をチェックし、
  // チェックボックス・コンボボックス・セレクトリストを使用不能とする
  function checkChkbox(){
  chkList=[]; // 必ず初期化する必要がある
  $("#fm2_725 input:checkbox:checked").map(function(){
   chkList.push( this.value );
  }); // オンされたチェックボックスリストを配列 chkList に格納
  if (!chkList.length) { // No check 時には警告を発し、アニメを起動させない。
   alert("アニメ対象プロパティを何か 1 つ以上指定してください");
   return;
  }
  elementsDisabled("disabled"); //全て使用不可とする
 }

 // 囲み内ボタンの使用可確認とフォーム要素のカバー不透明度設定
 $done.attr("disabled","");
 $initAnim.attr("disabled","");
 elementsDisabled(""); // 起動時には form elements を全て使用可能とする
 $.each([$shutter1,$shutter2,$shutter3],function(){
  if (this.css("opacity")==1) this.css("opacity","0.5");
 });
 // 2 つの form 内の、2 つのボタンの使用可/不可状態の設定とシャッターアニメの起動設定
 function setForm(type,duration,easing){
  $done.attr("disabled",type);
  $initAnim.attr("disabled",type);
  var disp = (type==="disabled") ? "show" : "hide";
  if ( disp=="show" && odd[0] !== odd[1] || disp=="hide" && !odd[0] === !odd[1] ){
    $shutter1.animate({width:disp,height:disp},duration,easing);
    $shutter2.animate({width:disp,height:disp},duration,easing);
  }
  $shutter3.animate({width:disp,height:disp},duration,easing);
 }

 // アニメが動作中か否かを確認し、状態に応じて form 要素の使用不可/可を切り替える
 function isbusy(){
  ival =setInterval(function(){
   if ( $box1.is(":animated") || $box2.is(":animated") || $box3.is(":animated") || $box4.is(":animated") ) {
    setForm("disabled","slow",selEasing);
   } else {
    setForm("","slow",selEasing);
    clearInterval(ival);
    ival=undefined;
   }
  },20);
 }

 $done.toggle(function(){ // click toggle
  odd[0]=!odd[0]; // 奇数か偶数かを反転させる
  isbusy();
  checkChkbox();
  goAnima("toggle");
  $done.attr("value","最後まで動かす");
  $(this).blur();
 },function(){ // 奇数回目はe.hide 、偶数回目はe.showメソッドとなる
  if (isbusy()) return;
  odd[1] = !odd[1]; // 奇数か偶数かを反転させる
  for (var p in animaCSS) animaCSS[p] = odd[1] ? "hide" : null;
  $.each([$box1,$box2,$box3,$box4],function(i,n){
   if (!odd[1]) { // 偶数回目つまり e.show メソッドの2度目の起動前に
    for (var p in animaCSS) // 終了値を強制的にアニメ開始値とする
     animaCSS[p] = initCSS[i][p];
   }
   this.animate(animaCSS,dur,selEasing,function(){ // complete funcを指定
    for (var j=0; j<attrs.length; j++) // CSS style オブジェクトの各値を初期値に戻す。
     $.attr(n.get(0).style, attrs[j] ,initCSS[i][attrs[j]]);
    if (i==3) { // 75%ボックスが最後まで表示されたら
     $done.attr("value",odd[1] ? "Do 表示アニメ" : "Do 隠蔽アニメ");
     elementsDisabled(""); //全て使用可とする
    }
   });
  });
  $(this).blur();
 });
})();

jquery.js におけるアニメーションコードの解読 ( 7 )

ボックスアニメーションのサンプル

このエントリイでは jquery.js におけるアニメーションコードのこれまでの解読を踏まえて、一寸一息いれることとします。アニメサンプルを多数用意して、その解説を試みます。

ここでいう、ボックスアニメーションとは私が勝手に名付けた名称ですが、配置指定された div 要素のアニメーションを意味しています。

個々のボックスのアニメーションを 12 種類、その他これらのボックスを利用したアニメーションを 6 種類、計 18 種類のアニメを作成し、その挙動に関する解説を行い、末尾には作成したスクリプトを付けました。

18 種類のアニメーションサンプル

下のボタンをクリックすると隠蔽/表示アニメーションが起動します。 各々のボタンには、隠蔽と表示の 2 機能を持たせました。

ぼっくす 1
toggle()
ぼっくす 2
toggle(2000)
ぼっくす 3
起終点:中心
ぼっくす 4
起終点:右上
ぼっくす 5
起終点:右下
ぼっくす 6
起終点:左下
ぼっくす 7
起終線:上
(文字も変化)
ぼっくす 8
起終線:左
(文字無変化)
ぼっくす 9
起終点:左上(文字も変化)
ぼっくす 10 width だけ toggle
ぼっくす 11 borderWidth だけ toggle
ぼっくす 12 borderWidthをAnim 

▲ToTop

18 のサンプルアニメーションの解説

1 : $().toggle() の挙動

jQuery インスタンスの toggle メソッドの例です。引数なしでこのメソッドを起動すると、$(htis).is(":hidden") メソッドによって当該要素の隠蔽/表示状態をチェックします。

その結果に応じて、もし true ならば $(this).hide() が、あるいは false ならば $(this).show() がそれぞれ起動され、それぞれのメソッドによって、this.style.display.none と this.style.display.block の切り替えが行われ、隠蔽と表示がアニメートされます。「時間」に係るコードは全く含まれないので、瞬時に行われる単純なアニメとなります。

2 : $().toggle(2000) の挙動

1 と同じ jQuery インスタンスの toggle メソッドの例ですが、動作時間を指定した引数がある点が異なります。

この引数があると toggle メソッドの定義から animate(genFx("toggle",3),"slow") メソッドが起動され、この genFx("toggle",3) 関数実行によって margin、padding 及び content サイズ( height と width )並びに opacity の 4 つの( 正確には margin と padding にはそれぞれ左右上下があるので、2×4+3 で 11種類の )プロパティがアニメーション対象となります。

ここで興味深いことは border がアニメ対象になっていないということです。当然のことですが、color プロパティと違って、border は jquery.js のアニメに馴染まない訳ではありません。実際 borderWidth を問題なくアニメ対象プロパティとして扱えることは、上のサンプル 11&12 で示した通りです。

※ 実は color をアニメ対象とする jquery 拡張版 ui があります。effects.core.js の中に含まれています。

そもそも、border には幅の他に、値が数値ではない色とスタイルの属性があります。そのため border そのものはアニメ対象プロパティから除外したのかもしれません。borderWidth だけでもアニメ対象プロパティにしてもおかしくないはずなのですが、fxAttrs の対象にしなかったのはどうしてでしょうか。理由は判然としません。

さて、上の 11 のアニメ対象プロパティを対象として、animate メソッドの沢山のコードが進行します。animate メソッドの第 1 引数に genFx("toggle",3) が指定されたことにより、jQuery.fx のインスタンス e から、e.hide() や e.show() メソッドが起動され、そこから e.step() メソッドが起動されます。

こうして、11 のプロパティの値が、当初の表示状態値と 1 ( e.show() メソッドにおいて width と height プロパティを扱う場合 ) 又は 0 ( その他のプロパティの場合 )の間を漸減/漸増させられて、アニメーションが引き起こされます。

最後に、$().toggle(2000) メソッドが、アニメ対象要素の左上に収斂し、その左上から展開されることに留意する必要があります。何故ならば、e.hide() や e.show() メソッドは、margin、padding、width、height を 0 に( e.show() メソッドの場合 width と height は 1 に )遷移させるのですから、左上に収斂しそこから展開することになるのです。

更に、これらのメソッドは不透明度も 0 と 1 の間を漸減/漸増させるため、不透明と透明の間を行き来することになります。

ここで触れたアニメの終点/起点の位置指定こそ、animate メソッドを扱う上での最も重要なポイントです。

▲ToTop

3 : ボックス中心を起終点とするアニメの挙動

ボックス 2 では左上がアニメの起終点でした。当然その起終点位置を変えてみたくなります。

「 $().toggle メソッドを使う前提で、どうすれば起終点位置を変えられるか? 」───かなり悩みました。toggle メソッドは e.show() や e.hide() メソッドを呼び出しますが、その中でプロパティ値は 0 又は 1 に変更されます。つまり起終点は左上です。そして toggle メソッドの定義から、何らかの引数により位置指定を変えることは不可能です。結局、位置指定は toggle メソッドでは行えません。このメソッドでは必ず左上に収斂し、そこから展開するアニメしか演出できません。

そしてさんざん考え抜いた結果ついに...。
「 位置指定を別のメソッドで行い、かつそれを toggle メソッドと並行起動すればよい」───このことに気がつきました。

起終点位置だけを指定するアニメーションメソッドは、基本的に top、left の初期値と終了値を適切に設定することになります。(アニメ対象要素は一般に position 指定を「絶対」か「相対」に指定しますので top 及び left が指定できます。)

具体的な対応方法はこのエントリイ最下部に掲載したコードを見て戴くとして、苦労した点を記録しておきます。

ボタンクリックでアニメを励起させていますが、そのボタンはボックスの表示アニメにも、あるいは隠蔽アニメにも、両刀使いするように設計してあります。

そして、初期状態がボックスの表示となっているのですから、toggle メソッドの別の使い方( click の度に 2 つの関数を交互に起動する toggle メソッド )を利用する方法もあります。しかしここでは、もっと一般的な方法を探りました。

ここで提示したアニメサンプルの場合、収斂/展開するアニメーションの起終点(線)を同一にしてあります。それが自然だからですが、同一の点や線から始まり/終わるアニメの場合、その初期値と終了値は正負の符号が異なる同一の絶対値となります。そこで、ボタンのクリック回数が奇数回か偶数回かによって、外サイズ取得値の正負を反転させて、起終点( あるいは起終線 )位置とするようコーディングしました。

▲ToTop

4 : ボックス右上を起終点とするアニメの挙動

今度はボックスの右上を起終点とするアニメです。style.top ±= 0 及び style.left ±= box 外幅から、起終点値を出しました。

隠蔽/表示のトグル起動は 3 と同様の奇数回/偶数回による方法で対処しました。以下 5、6 共同様。

5 : ボックス右下を起終点とするアニメの挙動

ボックスの右下を起終点とするアニメです。style.top ±= box 外高さ及び style.left ±= box 外幅から、起終点値を出しました。

6 : ボックス左下を起終点とするアニメの挙動

ボックスの左下を起終点とするアニメです。style.top ±= ボックス外高さ 及び style.left ±= 0 により起終点値を出しました。

7 : ボックス上辺を起終線とするアニメの挙動

ここからアニメの趣が変わります。これまでは全て $().toggle メソッドを利用してきました。起終点位置を ( 0, 0 ) 以外とするために animate メソッドを併行起動させた場合も、全て toggle メソッドとの併行起動でした。

ところが 7、8、10 ~ 12 番目のサンプルは toggle メソッドを使っていません。但し、12 番目のサンプル以外はアニメ用 CSS オブジェクトのプロパティ値を "toggle" としていますので、プロパティ値を "toggle" とした場合の animate メソッドのコーディングによって、不透明度はアニメ対象になりませんが、縮小するアニメの場合には必ず最後に消滅し、展開する場合には最初に表示されます。

さて、7 番目のサンプルでは、縦方向のサイズに係るプロパティだけ( height、marginTop、marginBottom、paddingTop 及び paddingBottom )をアニメ対象とした上で、更に文字サイズもアニメ対象に追加してみました。

起こされるアニメは slideUp メソッドに酷似していますが、slideUp メソッドは使用せず、fxAttrs 配列を拡張した exAttrs と、genFx と全く同一な Fx メソッドを使用してアニメートさせました。
( ここに独自に定義した配列とメソッドは、共に無名関数内のローカル変数としました。)

▲ToTop

8 : ボックス左辺を起終線とするアニメの挙動

7 の変形でボックス左辺に畳み込み、そこから拡幅するメソッドです。文字サイズはアニメ対象としていません。アニメ用プロパティ値に "toggle" を指定しているので、縮小後に隠蔽され( display.none )、展開直前に表示が再開( display.block )されます。

9 : ボックス左上を起終点とし文字サイズも変えるアニメの挙動

このアニメも exAttrs と Fn を活用して極めて短いコーディングで作りました。ボックス 2 の挙動に文字の変化を加えたものです。

10 : width だけをアニメ対象プロパティとした挙動

アニメプロパティとして width だけを対象と、animate メソッドの第 1 引数であるアニメ用 CSS プロパティ値を "toggle" としました。従って、不透明度はアニメ対象とはなりませんが、縮小後に隠蔽され、展開直前に表示が再開されます。

11 : borderWidth をアニメ対象とした挙動

jquery.js では borderWidth を標準的なアニメ操作対象としていないので、敢えてそれを対象としたアニメを作ってみました。

ここでも、animate メソッドの第 1 引数であるアニメ用 CSS プロパティ値を "toggle" としたので、不透明度はアニメ対象とはなりませんが、外枠線が細くなっていって最終的にボックスは hide され、他方、枠線が太くなる前にボックスが show されます。

12 : borderWidth だけを animate させ、かつ step メソッドを利用した挙動

最後のサンプルはボーダーサイズだけを animete メソッドの対象とし、かつ、toggle 値を使っていないので、border 幅がゼロに縮小されても決してボックスは隠蔽されません。

また、animate メソッドの第 2 引数 speed をオブジェクトとして指定し、このサンプルだけ唯一 e.options.step メソッドを利用して、アニメーションの起動回数をカウントさせました。こうして 12 のサンプルの中で最もカスタマイズ度の高いものになりました。

アニメーションを起動すると、起動回数がボックス内文字列の最後に追加され、ストップウォッチのようにカウンタ数が変化します。

この animate メソッドではアニメ対象プロパティを 4 つ指定していますので、各プロパティ毎の起動回数は、カウンタ値を 4 で割れば算出できます。

なお、アニメーションの起動回数は、duration 指定時間、CPU の性能、メモリの空き具合、ブラウザの Javascript 解析/実行性能等に左右されますから、同じ duration 指定であってもブラウザとパソコンの性能毎に異なることになります。

▲ToTop

13 : 全てのアニメを一斉に起動する挙動

12 番目までのサンプルを一斉にアニメーションさせたいと思い、それを実行しました。

スクリプトは、ボタン click メソッドの引数において、各アニメ起動関数を対象として each メソッドで巡回処理しているだけです。

14 : 全てのアニメを順次起動する挙動

12 番目までのサンプルを一斉にアニメーションさせた次には、一斉ではなく順次起動させることもおもしろい、と思い立ちそれを実行しました。

スクリプトでは、ボタン click メソッドの引数における 12 番目までのアニメ起動関数を、setTimeout 関数を使って一定間隔で遅延させながら巡回処理しました。

15: ランダムに 2 つのボックスを選択させて入れ替える挙動

一斉及び順次起動するアニメーションの次には、乱数関数を使って任意に 2 つのボックスを選択させ、それらの位置を互いに入れ替えるアニメーションを考えました。2 つのボックスの top 値と left 値を相互に入れ替えるだけなので決して複雑なコーディングは要りません。

なお、ここでは 弾性的動きを引き起こす easing 関数を採用してみました。この結果「イヤイヤしながら仕方なく移動する」ような動きとなりました(爆爆WWW)

16: 6 つのボックスを同時に入れ替える挙動

2 つのボックス入れ替えをやってみたら、もっと多くのボックスを同時に入れ替えてみたいと思い、それを実現しました。

注意した点は、必ず 異なる 6 つのボックスを選択させなければならない、ということです。1 ~ 12 迄の乱数を乱数関数で発生させ、そのうちの 6 個を使うわけですから、関数からの返値が同じ値になる可能性は非常に高いわけです。

大変冗長なコードになりましたが、返値について 1 つずつ同じかどうかチェックし、同じ場合には乱数発生をやり直して、必ず 6 つの値が異なるようにしました。

なお、折角の機会なので easing 関数は全て異なるものを使ってみました。

17: ボックス位置交換をタイムラグを持たせて行う挙動

更に、ボックス入れ替えを同時にではなく、タイムラグを持たせて実行してみたい、と思いそれを実現しました。

タイムラグは setTimeout 関数で animate メソッドの起動を遅延させて実現しました。

18: ボックスの並び順を整列する挙動

12 個全てのボックスを瞬時に番号順に整列します。ボックスの番号を頼りに top 値と left 値を算出する代数式を考え、コーディングして全ボックスを瞬時に整列するようにしました。入れ替わった箇所だけを元に戻すのは大変なので、簡便で安易な方法を採用しました(^^;。

上の 18 アニメサンプルに関するスタイルシートとスクリプト

■ スタイル設定
 #animArea {
  width:485px; height:440px;margin-top:1em;
  position:relative; background:lightcyan;
  padding:10px;
  text-align:center;
  line-height:1.2em;
 }
 button.btn {margin:5px; overflow:visible; padding:2px;}
 #box1_724, #box2_724, #box3_724, #box4_724, #box5_724, #box6_724,
 #box7_724, #box8_724, #box9_724, #box10_724, #box11_724, #box12_724 {
  position:absolute;left:10px;
  margin:10px 10px 10px 10px;
  border:palegoldenrod 10px ridge;
  padding:5px;
  width:110px; height:60px;
  background:royalblue;
 }
 #box4_724, #box5_724, #box6_724 {
  top:120px;
 }
 #box7_724, #box8_724, #box9_724 {
  top:230px;
 }
 #box10_724, #box11_724, #box12_724 {
  top:340px;
 }
 #box2_724, #box5_724, #box8_724, #box11_724 {
  left:170px;
  border:lightblue 10px outset;
 }
 #box3_724, #box6_724, #box9_724, #box12_724 {
  left:330px;
  border:plum 10px inset;
 }
■Javascript
(function(){
 var exAttrs = [
  // height animations
  [ "height", "marginTop", "marginBottom", "paddingTop", 
   "paddingBottom", "borderTopWidth","borderBottomWidth" ],
  // width animations
  [ "width", "marginLeft", "marginRight", "paddingLeft",
   "paddingRight", "borderLeftWidth","borderRightWidth" ],
  // opacity animations
  [ "opacity" ],
  // fontsize animations
  [ "fontSize" ]
 ];
 function Fn( type, num ){
  var obj = {};
  jQuery.each( exAttrs.concat.apply([], exAttrs.slice(0,num)), function(){
   obj[ this ] = type;
  });
  return obj;
 }
 var i=1,j=13,$btn=[],$box=[],hidden=[],even=[],w=[],h=[],flag=[],duration=2000;
 for (; i<j; i++) {
  $btn[i] = $("#btn" + i +"_724");
  $box[i] = $("#box" + i + "_724");
  hidden[i] = $box[i].is(":hidden");
  w[i] = !hidden[i] && $box[i].outerWidth() || 0;
  h[i] = !hidden[i] && $box[i].outerHeight() || 0;
  flag[i]=0;
 }
 for (var i=13; i<19; i++) {
  $btn[i] = $("#btn" + i +"_724");
 }
 $("button.btn").hover(
 	function(){$(this).css("background-color","paleturquoise")},
 	function(){$(this).css("background-color","")}
 ).toggle(
  function(){$(this).css("background-color","thistle")},
  function(){$(this).css("background-color","palegreen")}
 );

 if(!$box[12].is(":hidden")) {
  var bordLeft =  $box[12].css("border-left-width"),
  bordRight = $box[12].css("border-right-width"),
  bordTop = $box[12].css("border-top-width"),
  bordBottom = $box[12].css("border-bottom-width");
 }
 $box[12].append("<span/>");

 function anim1(){$box[1].toggle();}
 function anim2(){$box[2].toggle(duration);}
 function anim3(){
  var even = ++flag[2] % 2 === 0,
   leftAnim=( ( hidden[3] + even ) ? "-=": "+=" ) + w[3]/2 +"px",
   topAnim=( ( hidden[3] + even ) ? "-=" : "+=" ) + h[3]/2 +"px";
  $box[3].toggle({queue:false,duration:duration})
   .animate( {left:leftAnim, top:topAnim},duration );
 }
 function anim4(){
  var even = ++flag[3] % 2 === 0,
   leftAnim=( ( hidden[4] + even ) ? "-=": "+=" ) + w[4] +"px";
  $box[4]
   .animate( {left:leftAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim5(){
  var even = ++flag[5] % 2 === 0,
   leftAnim=( ( hidden[5] + even ) ? "-=": "+=" ) + w[5] +"px",
   topAnim=( ( hidden[5] + even ) ? "-=" : "+=" ) + h[5] +"px";
  $box[5]
   .animate( {left:leftAnim, top:topAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim6(){
  var even = ++flag[6] % 2 === 0,
   topAnim=( ( hidden[6] + even ) ? "-=" : "+=" ) + h[6] +"px";
  $box[6]
   .animate( {top:topAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim7(){$box[7].animate(
  $.extend(Fn("toggle",1),{fontSize:"toggle"}),duration
 )}
 function anim8(){$box[8].animate({
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",
  paddigLeft:"toggle",paddingRight:"toggle",borderLeftWidth:"toggle",
  borderRightWidth:"toggle"},duration
 )}
 function anim9(){$box[9].animate(Fn("toggle",4),duration);}
 function anim10(){$box[10].animate({width:"toggle"},duration)}
 function anim11(){$box[11].animate({
  borderLeftWidth:"toggle", borderRightWidth:"toggle",
  borderTopWidth:"toggle", borderBottomWidth:"toggle"
 },duration)}
 function anim12(){
  var k=0, even = ++flag[12] % 2 === 0,
  leftAnim = (( hidden[12] + even ) ? "+=": "-=") + bordLeft;
  rightAnim = (( hidden[12] + even ) ? "+=": "-=") + bordRight;
  topAnim = (( hidden[12] + even ) ? "+=": "-=") + bordTop;
  bottomAnim = (( hidden[12] + even ) ? "+=": "-=") + bordBottom ;
  $box[12].animate({
    borderLeftWidth :leftAnim, borderTopWidth:topAnim, 
    borderRightWidth:rightAnim, borderBottomWidth:bottomAnim
   },{
	  duration:duration, step : function(){k++; $(this).children().text(k);}
   }
  );
 }

 var animList =[anim1,anim2,anim3,anim4,anim5,anim6,anim7,anim8,anim9,anim10,anim11,anim12];
 $.each(animList,function(i,n){ $btn[i+1].click(n) });
 $btn[13].click(function(){$.each(animList,function(i,n){n()})});
 $btn[14].click(function(){
  $.each(animList,function(i,n){setTimeout(n,400*i)})
 });
 // 乱数を使って 2 つのボックスの位置を交換する
 $btn[15].click(function(){
  var A = parseInt(Math.random()*12)+1,
    B = parseInt(Math.random()*12)+1,posA={},posB={};
  while ( A===B ){
    B = parseInt(Math.random()*12)+1;
    if (A!==B) break;
  }
  var $boxA=$("#box"+A+"_724"),$boxB=$("#box"+B+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
 });
 // 乱数を使って 6 つのボックス位置を一瞬で入れ替える
 $btn[16].click(function(){
  var A = parseInt(Math.random()*12)+1,
   B = parseInt(Math.random()*12)+1,
   C = parseInt(Math.random()*12)+1,
   D = parseInt(Math.random()*12)+1,
   E = parseInt(Math.random()*12)+1,
   F = parseInt(Math.random()*12)+1,
   posA={},posB={},posC={},posD={},posE={},posF={};
  while ( A===B ){
   B = parseInt(Math.random()*12)+1;
   if (A!==B) break;
  }
  while ( C===A || C===B){
   C = parseInt(Math.random()*12)+1;
   if (C!==A &&C!==B) break;
  }
  while ( D===A || D==B || D===C){
   D = parseInt(Math.random()*12)+1;
   if (D!==A && D!==B && D!==C) break;
  }
  while ( E===A || E==B || E===C || E===D){
   E = parseInt(Math.random()*12)+1;
   if (E!==A && E!==B && E!==C && E!==D) break;
  }
  while ( F===A || F==B || F===C || F===D || F===E){
   F = parseInt(Math.random()*12)+1;
   if (F!==A && F!==B && F!==C && F!==D && F!==E) break;
  }
  var $boxA=$("#box"+A+"_724"), $boxB=$("#box"+B+"_724"),
   $boxC=$("#box"+C+"_724"), $boxD=$("#box"+D+"_724"),
   $boxE=$("#box"+E+"_724"), $boxF=$("#box"+F+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
  $boxC.animate({top:$boxD.css("top"),left:$boxD.css("left")},duration,"easeInOutExpo");
  $boxD.animate({top:$boxC.css("top"),left:$boxC.css("left")},duration,"easeInOutQuart");
  $boxE.animate({top:$boxF.css("top"),left:$boxF.css("left")},duration,"easeInOutCirc");
  $boxF.animate({top:$boxE.css("top"),left:$boxE.css("left")},duration,"easeInOutBack");
 });

 // ボックス位置交換をタイムラグを持たせて行う
 $btn[17].click(function(){
  var A = parseInt(Math.random()*12)+1,
    B = parseInt(Math.random()*12)+1,
    C = parseInt(Math.random()*12)+1,
    D = parseInt(Math.random()*12)+1,
    E = parseInt(Math.random()*12)+1,
    F = parseInt(Math.random()*12)+1,
    posA={},posB={},posC={},posD={},posE={},posF={},delay=800;
  while ( A===B ){
   B = parseInt(Math.random()*12)+1;
   if (A!==B) break;
  }
  while ( C===A || C===B){
   C = parseInt(Math.random()*12)+1;
   if (C!==A &&C!==B) break;
  }
  while ( D===A || D==B || D===C){
   D = parseInt(Math.random()*12)+1;
   if (D!==A && D!==B && D!==C) break;
  }
  while ( E===A || E==B || E===C || E===D){
   E = parseInt(Math.random()*12)+1;
   if (E!==A && E!==B && E!==C && E!==D) break;
  }
  while ( F===A || F==B || F===C || F===D || F===E){
   F = parseInt(Math.random()*12)+1;
   if (F!==A && F!==B && F!==C && F!==D && F!==E) break;
  }
  var $boxA=$("#box"+A+"_724"), $boxB=$("#box"+B+"_724"),
    $boxC=$("#box"+C+"_724"), $boxD=$("#box"+D+"_724"),
    $boxE=$("#box"+E+"_724"), $boxF=$("#box"+F+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
  setTimeout(function(){
   $boxC.animate({top:$boxD.css("top"),left:$boxD.css("left")},duration,"easeInOutExpo");
   $boxD.animate({top:$boxC.css("top"),left:$boxC.css("left")},duration,"easeInOutQuart");
  },delay);
  setTimeout(function(){
   $boxE.animate({top:$boxF.css("top"),left:$boxF.css("left")},duration,"easeInOutCirc");
   $boxF.animate({top:$boxE.css("top"),left:$boxE.css("left")},duration,"easeInOutBack");
  },delay*2);
 });

 // ボックスを整列する
 $btn[18].click(function(){
  for (var i=1; i<13; i++){
   $box[i].css({top:110*parseInt((i-1)/3)+10 +"px",left:(i-1)%3*160+10 +"px"});
  }
 });
})();

▲ToTop

jquery.js におけるアニメーションコードの解読 ( 6 )

jQuery().animate(prop, speed, easing, callback)───その 3

このエントリイでは下の模式図における色づけ部分( e.custom → e.step → e.update )、すなわち二重巡回処理内での「初期値設定、終了値設定、経過時刻取得、その時点の要素表示」という一連の流れを跡付け、愈々 「 動き 」 演出過程の核心に迫ります。

$().animate(prop,speed,easing,callback){
 $().each(func(){ || $().queue(func(){ //イテレートその1( 対象要素毎 )
  $.each(prop,func(name,val){ //イテレートその2( CSSスタイルプロパティ毎 )
■このエントリイで解読する部分
  ▼ val が toggle || show || hide 
     → e.show || e.hide メソッド起動
      これらのメソッド内で最終的に e.custom メソッドを起動
  ▲ val がその他ならば
     → e.custom メソッドを起動

  ● e.custom =初期値を取得し、アニメ開始時刻を記録する。
     ↓
     e.step =開始時刻からの経過時刻、その時点におけるCSS値などを設定する。
      ↓
      e.update =その時点のCSS値に基づいて要素を表示する。
  }); // prop 巡回function  }); // jQueryインスタンスに登録された要素を巡回するfunction };

jQuery.fx(elem, options, prop) コンストラクタと e インスタンス

引数:順にタグ要素、オブジェクト、プロパティ名称

役割:アニメーションを具体化する各種メソッドやプロパティを持つインスタンスを生成する。

このコンストラクタに new 演算子を適用して生成されるインスタンス e は、当初はコンストラクタの定義から次の 3 つのプロパティを持ちます。

 e ={
   elem : 第 1 引数, options : 第 2 引数, prop : 第 3 引数, options.orig : {}
 }

その後、各種メソッドによる e のプロパティの拡張/追加や、当該コンストラクタの prototype オブジェクトも定義されるので、e オブジェクトは最終的に次のように拡張されます。こうして e オブジェクトは現在進行中のアニメに係る各種情報を満載したオブジェクトになります。また、#4151 ~ 4171 によって、jQuery.fx クラスは 2 つのクラスプロパティを与えられます。

これらのことを模式図的にまとめると以下のようになります。

■ jQuery.fx コンストラクタの e インスタンス
 e = {
 elem:elem,// 第 1 引数
 prop:propName, // 第 3 引数
 options: {
  queue:false || true, step:function(){・・・}, // これらはユーザーが指定する。
  old,complete,easing,duration,display,overflow,// animateメソッドから引き継がれる。
  curAnim, //animateメソッドの第一引数である prop オブジェクトが格納される。
  orig:{prop},
  show:true || false || undefined, hide:true || false || undefined
 },
 startTime, start, end, unit, now, pos, state,
 // メソッド
 cur(), custuom(), step(), update(), show(), hide()
}

■ jQuery.fx コンストラクタのクラスプロパティ
 jQuery.fx.speeds = {"slow":600, "fast":200, _default:400}
 jQuery.fx.step = {opacity:fn(e){・・}, _default:fn(e){・・}}

それでは上の「 このエントリイで解読する部分 」 の模式図に従って、e インスタンスの各種メソッドを跡付けることにします。最初に登場するのは e.show() メソッドです。

▲ToTop

はじめに ─── $().show() / $().hide() と e e.show() / e.hide()

これから解読する e.show() メソッドと e.hide() メソッドは、jQuery インスタンスから起動される show() メソッド、hide() メソッドと表現が酷似しています。最初は余りに似ているこの表現に戸惑いました。

jQuery インスタンスの show / hide メソッドを引数なしで起動した場合には、displayプロパティが none 以外( show の場合 )、あるいは none( hide の場合 )に設定されて、対象タグ要素が瞬時に表示 / 隠蔽されます。

これに対して、jQuery インスタンスの show / hide メソッドをアニメ所要時間などの引数付きで起動した場合には、animate メソッドが呼び出されて、そこから更に e.show / e.hide メソッドが呼び出されます。この時 e.show / e.hide メソッドは決して display 値を none とすることはなく、常に表示モードのママ保たれます。

瞬時アニメと時間を掛けて行うアニメ動作では、時間要素の有無という根本的な差異があるため、スクリプト動作も本質的に変わります。

jQuery インスタンスの show / hide メソッドは、style.display プロパティの none と block、inline などの値を使用して表示と隠蔽を操作し、これに対して e.show / e.hide メソッドは、時間を掛けて当該タグ要素の大きさ等に係る各種 CSS プロパティ値(マージン、パディング、内容及び不透明度の他、ボーダーサイズ、フォントサイズ等々も対象となる)を操作して、要素の表示状態を変化させます。ここでは表示状態を操作するのであって、決して要素を隠蔽することはありません。

e.show() メソッド

引数:なし

返値:なし

機能:対象要素の対象 CSS プロパティ値の、初期値( 0 又は 1 )と終了値を設定し、これらを custom メソッドに渡します。こうして最小或いは透明な表示状態 =「 実質的な非表示状態 」から、表示状態へと移行するアニメーションが実現されます。

このメソッドは対象要素に対して、「実質的な」非表示状態からアニメ前にブラウザが描画していた値までのアニメーションを引き起こします。その進行は次の 3 過程に分けられます。

第 1 過程( #4070~4072 )はアニメ開始前の style.properties 値の記憶です。アニメーション開始前に、アニメ対象タグ要素 elem の elem.style.properties を、e.options.orig プロパティに待避/記憶させます。このメソッドから最終的に起動される update メソッドが、elem.style.properties を書き換えてアニメーションを実現するので、この過程が必要となります。

また、CSS プロパティ値を記憶させた後に e.options.show 値を true に設定し、e.show() メソッドが起動されたことも記憶させます。

第 2 過程( #4077 )では、対象 CSS プロパティの初期値と終了値を custom メソッドに渡します。初期値はプロパティが width または height の場合には 1 、その他の場合には 0 とし、終了値は e.cur メソッドを起動して取得します。こうして得られた初期値と終了値を引数にして custom が起動されます。

ここに対象 CSS プロパティの初期値を 1 又は 0 とするのは、言うまでもなく当該要素のアニメーションを「実質的な非表示状態」から開始するためです。

第 3 過程は $(this).show()メソッドの起動です。これにより custom メソッドが終了し、既に当該要素が表示状態になっていることを "olddisplay" という名称で当該要素に関連づけます。

4068: // Simple 'show' function
4069: show: function(){
4070:  // Remember where we started, so that we can go back to it later
     // 4077 行でスタイル属性値を 1 又は 0 ゼロにするので、現在値を記憶させておく。
     // options.orig オブジェクトの prop プロパティに
     // 当該要素のスタイル属性オブジェクトの prop プロパティの値を代入する。
4071:  this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
4072:  this.options.show = true; // e.show メソッドが起動されたことを記録する。
4073:
4074:  // Begin the animation
4075:  // Make sure that we start at a small width/height to avoid any
4076:  // flash of content
     // ちらつきを避けるために幅と高さの最小値はゼロとせず、その他のスタイル
     // 属性値は CSS 値はゼロとして start CSS 値を設定し、end CSS 値をカレント
     // スタイル値として、custom メソッドを起動する。
4077:  this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur());
4078:
4079:  // Start by showing the element
4080:  jQuery(this.elem).show(); // jQuery インスタンスメソッド show()の起動により、
4081: },// 当該要素が表示状態にある等の情報を記録させる。

▲ToTop

e.hide() メソッド

引数:なし

返値:なし

機能: e.show() メソッドの逆のことを行います。表示状態から「 実質的な 」非表示状態へと移行するアニメーションを引き起こします。

このメソッドの進行過程は単純です。最初に行うことは、e.show() メソッドと全く同一です。当初描画時の CSS プロパティ値を e.options.orig プロパティに待避/記憶させ、かつ、e.options.hide 値を true に設定し、e.hide() メソッドが起動されたことも記憶させます。(#4085~4087)

次に、 custom メソッドに対象 CSS プロパティの初期値と終了値を渡して、表示状態から隠蔽状態へ移行するアニメーションを励起します。この初期値は e.cur() メソッドを起動させて現在表示値を取得し、終了値は 0 です。(#4089~4090)

ここに、e.show メソッドの場合と異なり、非表示状態の CSS 値を全てゼロとしていることに留意する必要があります。

showメソッドの場合には、非表示状態から表示状態へ遷移する際の "ちらつき" を防止するため、プロパティ名が width と height の時にはプロパティ初期値を 1 としていましたが、e.hide メソッドの非表示状態値(=アニメ終了値)は、プロパティに関わらず全てゼロとしています。隠蔽する場合にはちらつき問題が発生しないから、全てのプロパティ値をゼロにしても支障がないのです。

それでも決して display 値は none にしていないことも同時に確認しておく必要があります。e.hide メソッドは要素を CSS 的には隠蔽せず、実質的な非表示状態で表示し続ける、と言えばよいでしょうか。

4083: // Simple 'hide' function
4084: hide: function(){
4085:  // Remember where we started, so that we can go back to it later
4086:  this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
4087:  this.options.hide = true; // this.options.hide プロパティを true とする。
4088:
4089:  // Begin the animation
     // CSS 現在値を start に、end 値をゼロとして custom メソッドを起動する。
4090:  this.custom(this.cur(), 0);
4091: },

▲ToTop

e.custom(from,to,unit) メソッド

引数:順に、初期値、終了値、単位

返値:なし

機能:初期値、アニメ開始時刻、e インスタンスのアニメ用の各種プロパティ( start、end、unit、now、pos 及び state )と 1 つの関数( t 関数 )を次々に設定し、その後 e.step() メソッドを起動して、インターバル関数によって 13 ミリ秒毎、当該要素の各プロパティ毎にプロパティ値を変化させ、もってそれを表示させてアニメーションを実現し、更に終了処理を行います。

このメソッドはその名の通り、カスタム ( slideUp や fadeOut などの既成アニメーションではない、ユーザー指定の ) アニメーションの中核を為します。その役割により 2 つのブロックに分けて考察します。

前半の過程
#4038 ~ 4050 では、現在対象としているプロパティのアニメ開始時刻、アニメ初期値、アニメ終了値、単位等を設定し、それを利用して演算する e.step メソッドを起動させる t 関数を定義します。また t 関数のプロパティとして対象要素を登録します。ここまでは非常に分かりやすい簡単なコード進行です。
後半の過程

#4050 ~ 4065 は、前半とは対照的に大変複雑です。

改めてアニメコードの全体を思い起こすと、今対象としている動作は animate メソッドの jQuery インスタンスの各要素毎の巡回処理の、その中での各 CSS プロパティ毎の巡回処理過程で引き起こされている事象です。この複雑さ故にコード進行過程が分かりにくいので、操作対象を以下のように可視化してみます。

対象としている jQuery インスタンスに登録されているタグ要素数を m 、アニメ用の CSS オブジェクトに登録されているプロパティ数を n として、或る t 関数がどの要素のどのプロパティのためのものなのか分かるように、以下のように表示します。但し、この数列に酷似した表示はあくまでも説明用です。

  • 最初の要素に対する最初のアニメプロパティ用 t 関数……e1 t p1
  • 最初の要素に対する 2 番目のアニメプロパティ用 t 関数……e1 t p2
  • j 番目の要素に対する k 番目のアニメプロパティ用 t 関数……ej t pk
  • m 番目の要素に対する n 番目のアニメプロパティ用 t 関数……em t pn

上のように各々が識別できる( m × n )個の t 関数を名付けた上で、#4052-4064 を解読します。

▲ToTop

後半の過程の詳説
  1. e1 t p1 、つまり最初の要素に対する最初のアニメプロパティに係る t 関数が 4052 行で実行されると、初めて step メソッドが起動され、その時の時刻が ローカル変数 t に記録されます。この 変数 t が custom メソッドで定義された e.startTime(s)に、animate メソッドの引数から定められた duration 時間(d)を加算した時刻( s + d )以内であれば ( つまり t ≦ s + d であれば )、step メソッドは、それ自信の中で算出するその時のプロパティ値に基づいて、対象要素の表示サイズなどを算出してから、update にそれを渡します。そして update メソッドは受け取った値からブラウザに要素を表示してから true を custom メソッドに返します。
  2. 4052 行において、t() に拠る返値が true となるので、jQuery.timers 配列の要素にその t 関数 e1 t p1 が追加され、3 番目の式 !timerId が評価されます。ここに、jquery.js トップレベルで定義されている変数 timerId は jquery.js コードの中で custom メソッドでしか使われていませんから、step メソッドから 1 つ目の true が返された時には、timerId は未定義です。つまり !timerId === true となります。こうして 4052 行の if 文が成立して、インターバル関数が起動され、以後は 13 ミリ秒ごとに t 関数が呼び出され、その返値の評価を行う行為が繰り返されます。
  3. 4052 行の if 文が成立した 13 ミリ秒後には、4056 行により timers 配列の 0 番目の要素である e1 t p1 が再度実行されます。このとき依然として 変数 t ≦ s + d ならば、step メソッド内で算出された値に基づいて、e1 要素の 2 度目の描画がおこなわれます。そして当該要素は 13 ミリ秒後に再び起動されるために配列内に残されたママとなります。他方 変数 t > s + d ならば、step メソッドが当該プロパティに係る終了処理 ( その内容については step メソッドで解説します ) を行ってから false を返すので、4057 行の if 文が成立して、e1 t p1 関数が配列から削除されます。(#4058)
    以上のような e1 t p1 の 13 ミリ秒毎の反復起動によって、e1 要素の変化が演出されます。
  4. さて、13ミリ秒毎の繰り返しと並行して、Javascript インタープリタは setInterval 関数を起動した直ぐ後に 同一要素の次のプロパティ(e1p2)を対象とした処理に進みます。3892 行の jQuery.each メソッドが 2 つめの prop を対象とした処理を行うのです。そして再び custom メソッドが起動され、その前半過程が処理され後半過程に入ってきます。今度 4052 行で起動される t 関数は e1 t p2 となり、再び jQuery.timers 配列にその t 関数 e1 t p2 が追加されます。
  5. この t 関数を追加した時点でまだ e1p1 のアニメが終わっていなければ 4052 行の if 文は成立しませんが、timers 配列には今追加したばかりの t 関数も含まれるので、13ミリ秒毎に繰り返される e1 要素のプロパティ値の変化過程が 2 つのプロパティで行われることになります。
  6. 他方、e1 t p2 関数追加時点で e1p1 に係るアニメが終わっていれば、4062 行により timerId は未定義となっているので、4052 行の!timerId が成立して、再びインターバル関数が 13 ミリ秒毎の t 関数実行を開始します。こうして最初の要素の 2 つ目のアニメプロパティに係る動きが演出されていきます。
  7. 以上のような過程が、対象とした jQuery インスタンスに格納されている m 個の要素毎の、それぞれ n 個のアニメプロパティにおいて繰り返されます。そして n × m 回の custom - step - update の連鎖起動の反復繰り返しを経て、遂に、対象とした jQuery インスタンスから起動された animate メソッドはその役割を終えます。
4036: // Start an animation from one number to another
4037: custom: function(from, to, unit){
4038:  this.startTime = now(); // 現在時刻を登録する。
4039:  this.start = from; // アニメ開始値
4040:  this.end = to; // アニメ終了値
4041:  this.unit = unit || this.unit || "px";
4042:  this.now = this.start; // = from となる。
4043:  this.pos = this.state = 0;
4044:
4045:  var self = this; // this は jQuery.fx クラスのインスタンス
4046:  function t(gotoEnd){ // t 関数を定義する
4047:   return self.step(gotoEnd); // step メソッドの実行結果を返す。
4048:  }
4049:
4050:  t.elem = this.elem; // t の elem プロパティ値に、this.elem を代入する。
4051:  // t 関数の実行結果が true で、t 関数を追加した timers 配列が存在し、
     // かつ timerId が存在しなければ
4052:  if ( t() && jQuery.timers.push(t) && !timerId ) {
4053:   timerId = setInterval(function(){
4054:    var timers = jQuery.timers;
4055:
4056:    for ( var i = 0; i < timers.length; i++ )
4057:     if ( !timers[i]() ) // timers[i] の実行結果が false ならば
4058:      timers.splice(i--, 1); // i 番目の要素を削除してから i を 1 減じる。
4059:
4060:    if ( !timers.length ) { // timers 配列の要素がなくなれば
4061:     clearInterval( timerId ); // timerId を停止し、
4062:     timerId = undefined; // timerId を未定義とする。
4063:    }
4064:   }, 13);
4065:  }
4066: },

▲ToTop

e.cur(force) メソッド

引数:強制的に算出スタイル値の取得を行う場合 true とする。

返値:スタイル値(タグ属性に記述されたスタイル値、又はスタイルオブジェクトで設定されているスタイル値、又は算出スタイル値)

機能:返値の説明の通り。

e.cur メソッドは、animate、e.show、e.hide などの各種メソッドから呼び出されて、タグ属性や style オブジェクトに設定されているスタイル値を取得し、あるいは指定があれば算出スタイル値を取得します。

まず、animate メソッドからは引数 true 付きで呼び出されます。(#3899)true 付きなので対象要素のアニメ開始前の算出スタイル値が強制的に取得され、その値がアニメ開始値とされます。

他方、e.show メソッド及び e.hide メソッドからは引数なしで呼び出されるので(#4077)、アニメ開始前の対象要素のタグ属性値又は style オブジェクト値が取得されて、アニメ終了値となります。

4027: // Get the current size
4028: cur: function(force){
     // 要素属性に当該プロパティ値が存在し、かつスタイル属性がないか、
     // あっても当該プロパティ値がない場合には
4029:  if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
4030:   return this.elem[ this.prop ]; // 要素属性値を返す。
4031:  // 当該要素の当該プロパティ値を単位名なしで取得する。
4032:  var r = parseFloat(jQuery.css(this.elem, this.prop, force));
     // r がゼロ以外で -1 万よりも大きければ r を返す。
     // さもなければ force 引数なしで当該要素の当該プロパティの CSS 値を取得する。
     // 取得できなければ 0 を返す。
4033:  return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
4034: },

▲ToTop

e.step(gotoEnd) メソッド

引数:gotoEnd を true に設定すると対象要素のアニメーションを停止させます

返値:false(アニメが終了した時) 又は true(アニメ実行中の時)

機能:2つの役割があり、第 1 はアニメ開始とアニメ終了の始点/終点間のプロパティ値を経過時間に基づいて刻むことです。この場合、返値 true を e.custom メソッドに返します。第 2 は、アニメ終了後の処理を行うことです。この場合返値 false を e.custom メソッドに返します。

このメソッドを理解する前提として、呼び出し元の e.custom メソッド及びその呼び出し元の animate メソッドによって、e.options オブジェクトの各種プロパティが既に拡張されていること、そしてそれらが e.step メソッドで縦横に利用されることを踏まえておく必要があります。

step メソッドはまず最初に、このメソッドが呼び出された時刻を変数 t に記録します。最初に行うこの作業は、時間という 「 変数 」 を管理し、要素の描画を自在に操作する上で不可欠です。

アニメーションを刻む場合

変数 t の値 ( step メソッドの起動時刻 ) が、e.custom メソッドが起動された時刻 ( startTime ) と e,options.duration の合算値よりも小さければ、すなわち、アニメが起動してからまだ duration で指定されている時間が経過しきらない内に step メソッドが起動された場合には、e.step メソッドはアニメーションを刻むためのプロパティ値の設定を以下のように行います。( #4134 ~ 4144 )

  1. #4135 の変数 n は経過時間です。
  2. #4136 の e. state は動作状態変数 ( 「 動作ステータス 」とも呼ばれるらしい )と呼ぶべきもので、duration 時間を 1 としたときの経過時間の割合を表します。0 ~ 1 未満の値となります。例えば e.state == 0.5 ならば ( その値は options.step メソッドによって取り出さない限りユーザーには見えませんが ) アニメが半分進行したその瞬間を表すことになります。
  3. #4139 の e.pos には n 、e.state 及び e.options.duration の 3 つの値から easing 関数を使用して、その時点における変化率を算出した結果が代入されます。なお、easing 関数は、(1)ユーザー指定のもの、(2)それがなければ jquery.js で定義されている swing、(3)何らかの理由によりそれも定義されていなければ linear が使用されます。裏返せば default easing は swingです。
  4. #4140 の e.now には直前で取得した変化率に、アニメによる変化量( 終了値 -開始値 ) を乗じて変化値を算出し、それを開始値に加算した結果が代入されます。
    こうして、その state におけるプロパティ値が得られます。
  5. 最後に #4143 にて e.update メソッドによりブラウザ表示を更新してアニメーションの刻みが終わります。

▲ToTop

アニメーションの終了処理を行う場合

(1) e.step の引数が true の場合、または (2) 変数 t の値 ( step メソッドの起動時刻 ) が、e.custom メソッドが起動された時刻 ( startTime ) と e,options.duration の合算値以上の場合には、すなわち、アニメ停止が指示されたか、またはアニメが起動してから既に duration 時間が経過してしまった時点で step メソッドが起動された場合には、e.step メソッドはアニメーション終了後の一連の終了処理を行います。( #4097 ~ 4134 )

  1. (#4098)その段階ではプロパティ値 e.now は e.end ( 既に設定済みの終了値 ) とします。
  2. (#4099)変化率 e.pos も 状態変数 e.state も共に 1 とすることによって、アニメーション進行中に既にこれらに代入されていた別の値 ( 共に 0 以上で 1 より小さい値 ) を上書きし初期化します。
  3. (#4100)以上の値によりブラウザ表示を更新します。これでアニメーションの最終形が描かれます。
  4. 次に、アニメーション終了処理、すなわちプロパティ値をアニメ開始前の値へ戻す初期化作業を行います。
    1. (#4102)まず、animete メソッドによって e.options.curAnim に登録済みの、今対象としているプロパティの値を true とします。これはそのプロパティのアニメーションが終わったことを意味する flag です。
    2. (#4104 ~ 4107)次に、ローカル変数 done を用意して値を true とします。この変数は次の for ループコードとセットになって、animate メソッドの第 1 引数で指定した prop オブジェクトの全てについて、それぞれがアニメーションを終えたかどうかが検証するために使われます。

      ひとつでもアニメーションを終えていないプロパティがあれば done は false となり、#4109 ~ 4131 は実行されないまま #4133 の false が e.custom メソッドに return されます。後処理を施さないまま step メソッドを終えてしまうわけです。( ※ これで果たして良いのか、疑問があるのですが解消出来ていません。)

    3. (#4110 ~ 4131)他方、上の #4104 ~ 4107 において全てのプロパティのアニメーション終了が確認されれば、つまり done が true のままならば、#4109 の if 文が成立してそれに続くブロックが実行されます。このブロックではアニメの対象となったプロパティを、アニメ開始前のプロパティ値に復元する ( 初期化すると言っても良い ) 作業が行なわれます。
      1. (#4110 ~ 4118)まず、e.options.display 値が空ではない場合の処理です。元々この値は animete メソッドの #3880 においてアニメ開始前の状態から待避/取得されています。もし、style.display があればその値が代入され、なければ算出スタイル値が代入されています。また、null 値以外の値とは、block、inline、inherit、none など display プロパティが取り得る値を指します。

        #4112 ではアニメ開始前に animate メソッドによって e.options.overflow に待避しておいた初期値を、対象要素の style.overflow プロパティに戻して復元します。

        次に、#4115 では、e.options.display に待避しておいた値を対象要素の style.display プロパティに戻します。その上で、#4116 ~ 4117 では、対象要素の display プロパティ値が "none" ならば ( null ではなく "none" であることに注意!! )、対象要素の style.display プロパティを "block" に書き換えます。
        この最後の処理は非表示になっている要素を強制的に「一旦」表示することになります。

      2. (#4121 ~ 4122)ここで先の「一旦」と記した意味が明らかになります。もし animate メソッドにおいて e.hide メソッドの起動があった場合には、当該要素を隠蔽します。どうして「一旦 block 表示させておき、その直後に隠蔽する」という煩わしい処理を行う必要があるのか───それは未解明です。
      3. (#4125 ~ 4127)animate メソッドにおいて e.show または e.hide メソッドが起動されていれば、e.options.curAnim オブジェクトに待避させておいた animate メソッドの第 1 引数のプロパティを巡回走査して、対象要素の style オブジェクトの当該プロパティ値を、e.options.orig オブジェクトの同一プロパティ名に格納しておいた値で上書きします。ここに e.options.orig オブジェクトには、e.show や e.hide メソッドによって、対象要素の style オブジェクトのアニメ前の当該プロパティ値が待避されています。───このような複雑な工程がどうして必要なのか未解明です。
  5. (#4130)最後に animate メソッドにおいてアニメ終了後に起動することを指定しておいた関数等を起動します。
■ e.step() メソッドコード
4093: // Each step of an animation
4094: step: function(gotoEnd){
4095:  var t = now(); // 現在時刻を代入する。
4096:  // gotoEnd があるか、t が所定時間よりも大きい場合には
4097:  if ( gotoEnd || t >= this.options.duration + this.startTime ) {
4098:   this.now = this.end;
4099:   this.pos = this.state = 1;
4100:   this.update(); // 要素のカレントサイズを測る。
4101:   // curAnim の当該プロパティ値を true とする。
4102:   this.options.curAnim[ this.prop ] = true;
4103:
4104:   var done = true;
4105:   for ( var i in this.options.curAnim )
4106:    if ( this.options.curAnim[i] !== true )
4107:     done = false; // trueでなければ
4108:
4109:   if ( done ) { // done===true ならば
4110:    if ( this.options.display != null ) { // display が null 値でないならば
4111:     // Reset the overflow
        // options の overflow 値を要素の overflow style 属性に代入する。
4112:     this.elem.style.overflow = this.options.overflow;
4113:
4114:     // Reset the display
        // options の display 値を要素の display style 属性に代入する。
4115:     this.elem.style.display = this.options.display;
        // display 値が none ならば
4116:     if ( jQuery.css(this.elem, "display") == "none" )
4117:      this.elem.style.display = "block"; // block とする。
4118:    }
4119:
4120:    // Hide the element if the "hide" operation was done
4121:    if ( this.options.hide ) // hide プロパティがあれば
4122:     jQuery(this.elem).hide(); // 隠す。
4123:
4124:    // Reset the properties, if the item has been hidden or shown
       // hide 又は show プロパティがあれば
4125:    if ( this.options.hide || this.options.show )
4126:     for ( var p in this.options.curAnim ) // curAnim オブジェクトを巡回走査
         // style 属性オブジェクトの p プロパティの値を options.orig[p] とする。
4127:      jQuery.attr(this.elem.style, p, this.options.orig[p]);
4128:     
4129:    // Execute the complete function
4130:    this.options.complete.call( this.elem ); // complete 関数を実行する。
4131:   }
4132:
4133:   return false; // false を返す。
4134:  } else {// 引数がないか、t が所定時間以下の場合には
4135:   var n = t - this.startTime; // 現在時刻とアニメ開始時刻との差を n に代入。
      // n を継続時間で除した値を state に代入。state は 1 未満の値となる。
4136:   this.state = n / this.options.duration;
4137:
4138:   // Perform the easing function, defaults to swing
      // easing 関数を実行する。それが指定されていなければ swing 関数を実行する。
      // その時の引数は順に this.state, n, 0, 1, this.options.duration
4139:   this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
      // 終了点から開始点を差し引いてから、easing 計算値を乗じた値を開始点に加算する。
4140:   this.now = this.start + ((this.end - this.start) * this.pos);
4141:
4142:   // Perform the next step of the animation
4143:   this.update(); // 次の段階へ進む。
4144:  }
4145:
4146:  return true; // true を返す。
4147: }

▲ToTop

e.update() メソッド

引数:なし

返値:なし

機能:アニメが進行している要素のブラウザ表示を更新します。

このメソッドは animate メソッドから呼び出される一連のメソッドの最後に位置しています。名前の通り表示を更新するメソッドですが、大切なことはこのメソッドのどこにも display = none が存在しないことです。

このメソッドは、step メソッドから時々刻々伝達される、遷移しつつある大きさや不透明度の値を受け取り、それに従って要素の表示状態を更新する役目を負っているのであって、例え各種プロパティの表示サイズがゼロとなり、不透明度がゼロとなった場合でも、display 値を block 、inline などの ( none ではない ) 表示用の値に設定したままで機能します。決して none にはしません。このことをしっかり踏まえなければ成りません。

  1. (#4017) e.options.step オブジェクトが存在すれば、e.now と e を引数にして、アニメ対象要素からそれを起動します。

    animate メソッドの第 4 引数や completeプロパティに指定する関数が、アニメーションが終わってから起動されるのに対して、この e.options.step は、アニメションの途中で何かを行うためにユーザーが指定する関数です。進行中のアニメーションの状態変数 e.state が変化する度に、何かを行わせたい時に使用します。

    なお、このメソッドの指定方法は queue プロパティと同様に speed オブジェクトのプロパティとして { queue:false, step:function(){}, duration:duration,・・・・} のようになります。この step 関数を使えば、アニメ中の各状態毎に「アニメ進捗状況に関する情報を表示する」、「背景色や文字色を変える」などが可能となります。とは言え色替えはとても推奨できません。変わる度にディスプレイ画面がちらつくからです。

  2. #4151 ~ 4171 によって jquery.js 読み込み時に speed と step プロパティが jQuery.fx オブジェクトに追加されます。 #4020 はそれを呼び出して使用する箇所です。

    ここに、jQuery.fx オブジェクトは #4001 ~ 4009 において定義されているコンストラクタ関数オブジェクトで、そのインスタンス e を縦横に駆使してアニメーションが実現されていることはこれまで見てきたとおりです。

    では jQuery.fx オブジェクトに追加された speed と step の 2 つのプロパティと、インスタンス e との関係はどうなるのか、直ぐに疑問が湧いてきます。

    答えは簡単で、e インスタンスにはこれらのプロパティは登録されません。これらがいわゆるクラスプロパティだからです。

    一寸脱線しましたが、 #4020 では、e.step メソッドから渡された値を基に、この jQuery.fx オブジェクトのクラスプロパティを利用してブラウザ上で要素を表示します。

    jQuery.fx.step クラスプロパティには、#4158 ~ 4170 により 2 つのメソッドが登録されています。jQuery.fx.step.opcity() と jQuery.fx.step._default() です。#4020 における引数 this が this の仕様から呼び出し元の e インスタンスを指していることを踏まえれば、4020行 は決して難解ではありません。但し、インスタンスメソッドである e.step とは全く別物であることに気をつけなければなりませんが...。

    前半の jQuery.fx.step[this.prop] は、アニメ対象プロパティ( this.prop )が opacity の時だけ true となり、jQuery.fx.step.opacity(this) を実行します。その結果、#4160 ~4162 によってアニメ対象要素の style オブジェクトの opacity プロパティが、fx.now となり、現在値の不透明度でアニメ対象要素が描画されます。( elem をアニメ対象要素として、elem.style.opacity = fx.now となります。)

    他方、e.step メソッドから渡されたアニメ対象プロパティ( this.prop )が opacity でない場合には、後半部分の jQuery.fx.step._default(this) が実行されます。_default() メソッドは、アニメ対象要素に style 属性が存在し、しかも style[ this.prop ] が空でない場合には、 style[ this.prop ] に単位付きで fx.now を代入します。

    また、アニメ対象要素に style 属性が存在しない場合、あるいは存在しても style[ this.prop ] が空の場合には、当該要素の this.prop 属性値を単位なしで fx.now とします。

    文章化するとわかりにくいのですが、アニメ対象要素が width の場合を例示すれば、前者は elem.style.width = fx.now+"px" であり、後者は elem.width = fx.now ということです。

  3. update メソッドの最後では(#4022 ~ 4025)、アニメ対象プロパティが height または width で、かつ、アニメ対象要素にスタイルオブジェクトが存在すれば、その display 値を block とします。

    このことの意味は、inline ではなく block にするということであり、決して none を block に変えることではありません。最初に触れたように、update メソッドは決してdisplay 値を none にすることはありません。飽くまでも「 表示 」を更新する役割を担っているからです。

4015: // Simple function for setting a style value
4016: update: function(){
     // options に step プロパティがあれば this.now と this を引数として
     // step を起動する。ここに step はユーザーが独自に指定するメソッドである。
4017:  if ( this.options.step )
4018:   this.options.step.call( this.elem, this.now, this );
4019:  // this を引数として jQuery.fx.step[this.prop] 関数
     // 又は jQuery.fx.step._default関数を実行する。
4020:  (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
4021:  // アニメ対象プロパティが height または width であって、
     // かつ、アニメ対象要素にスタイルオブジェクトが存在すれば
4022:  // Set display property to block for height/width animations
4023:  if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
4024:   this.elem.style.display = "block"; // style.display を "block" とする。
4025: },

▲ToTop

jquery.js におけるアニメーションコードの解読 ( 5 )

jQuery().animate() メソッド───その 2( 逐次実行と併行実行 )

このエントリイでは animate メソッドによって、複数のアニメーションを起動する 2 つの方法について考察します。複数アニメーションを、1 つずつ順番に起動する方法と、恰も同時に引き起こされているかのように並行して表示する─── こうした 2 つの異なるアニメ起動方法について、その差異をもたらすコード進行過程を考えてみます。

アニメーションの逐次起動はどのように行われるか?

【 opt.queue!==false ( opt.queue が true か undefined の時 )のコード進行 】

逐次実行アニメーションサンプル

本題に入る前に逐次実行アニメーションのサンプルを見てみます。

逐次実行アニメの一例

アニメの逐次実行とは、複数のアニメーションを 1 つずつ順番に実行することです。例えば、(1) 或るボックスを右に 200 ピクセル移動させ、(2) 背景色を変化させ、(3) 文字サイズ小さくしてからまた色を変更し、(4) 最後に文字サイズを大きくするアニメーションは以下のようになります。

なお、色は animate メソッドの操作対象外ですから、animate メソッドの引数である complete メソッドを利用してアニメートさせています。animate メソッドで作動させたアニメーションが終わる時に、色が変わるようにしたわけです。

 
逐次実行アニメ sample

上の逐次実行アニメーションのスタイルシートとスクリプトコードは以下のように作成しました。

■ スタイルシート
 #animTestIn722, #animTest2In722{
   width:200px;background-color:royalblue;
   border:2px white solid;font-size:1em;
   margin-left:100px;
 }

■スクリプトコード
var goSequential = function(){
 $("#animTestIn722").animate({marginLeft:"+=200px"},2000,
  function(){$(this).css({backgroundColor:"darkgreen"});})
  .animate({fontSize:"0.75em"},2000,
  function(){$(this).css({backgroundColor:"indigo"});})
  .animate({fontSize:"1.5em"},2000,function(){$(this).text("逐次実行アニメ終了")});
}
var backSequential = function(){
 $("#animTestIn722")
  .animate({fontSize:"0.75em"},2000,
   function(){$(this).css({backgroundColor:"darkgreen"});})
  .animate({marginLeft:"-=200px"},2000,
   function(){$(this).css({backgroundColor:"royalblue"});})
  .animate({fontSize:"1em"},2000,function(){$(this).text("逐次実行アニメ sample")});
}
var startSequential = function(){
 $("#animTestIn722").text("逐次実行アニメ sample")
  .css({backgroundColor:"royalblue",marginLeft:"100px",fontSize:"1em"});
}
var endSequential = function(){
 $("#animTestIn722").text("逐次実行アニメ終了")
  .css({backgroundColor:"indigo",marginLeft:"300px",fontSize:"1.5em"});
}
$("#btn1In722").click(function(){
 if ($(this).css("margin-left")!==100+"px") startSequential();
 goSequential();
});
$("#btn2In722").click(function(){
 if ($(this).css("margin-left")!==300+"px") endSequential();
 backSequential();
});
逐次実行アニメーションコードの解読

問題を単純化するために、まず、或るインスタンスから animate メソッドが初めて起動される場合を考えます。animate メソッドは非常に複雑なコード進行をするので、問題を単純化・細分化して考察する必要があるのです。

  1. animate メソッドの最初のサブルーチン speed メソッドが実行されると、opt.complete プロパティに、$(this).dequeue() と opt.old.call(this) の 2 つのメソッドが登録されます。なお、ここではあくまで登録が行われるだけで登録されたメソッドは実行されません。
  2. その後 $(this).queue メソッドが実行され、animate メソッドの唯一の引数である無名関数全体が、"fxqueue" 名で新たに関連づけられた配列に追加されます。これで関連づけ配列の要素は 1 つとなります。
  3. すると queue メソッドの定義により、queue メソッドによって当該配列に追加された当該無名関数が実行されます
  4. そしてこの無名関数が実行されて初めて、アニメーションを具体化するコードの履行が進み、そのアニメーションコード進行の最後段階で opt.complete メソッドが起動されると、opt.old に登録済みの、animeteメソッドの第 4 引数である callback 関数(すなわちアニメ終了後に起動することを命じられていた関数)が実行されます。
  5. このとき opt.old に登録済みのもう 1 つのメソッドである $(this).dequeue() メソッドは、引数がないので、インスタンスの要素毎に $.dequeue(this) が起動されます。その結果、各要素に既に "fxqueue" 名にて関連づけられている配列から、最初の要素が削除されます。この削除される最初の要素とは、2. で追加された関数そのものです。また $.dequeue メソッドが第 2 引数なしで呼び出されるため、関連づけ配列の 2 番目の要素があれば実行されますが、それは存在しないので、 $.dequeue(this) メソッドは、関連づけ配列の第一要素を削除する以外のことは何もしません。
  6. こうして $() インスタンスから起動された最初のアニメーションが終わります。

▲ToTop

次に、以上に続けて、同一インスタンスに 2 番目の animate メソッドが適用される場合を考えてみます。特に先行するアニメーションが未だ進行中の段階で、2 番目の animate メソッドが起動される場合を考えてみます。

アニメーションの逐次起動とは、このような場合でも、後のアニメーションが起動されないことなのですから...

  1. animate メソッドの最初のサブルーチン speed メソッドが実行されると、opt.complete プロパティに、$(this).dequeue() 及び opt.old.call(this) が登録されます。登録されるだけで実行されないことも含めて最初のアニメーションの場合と何ら変わりません。
  2. その後、$(this).queue メソッドが実行され、animateメソッドの唯一の引数である無名関数が関連づけ配列に追加されます。これも上と同一です。異なるのは、最初のアニメーションが作動中ですから、関連づけ配列に登録された無名関数はこの時点で 2 つとなります。まだ 5. による最初の要素の削除は起こっていないのです。するとこの時点では関連づけ配列の要素数は 2 以上となるので、$(this).queue メソッドの定義から 2 番目の無名関数の実行は引き起こされず、$(this).queue メソッドは関連づけ配列に 2 番目の無名関数を追加しただけでその役割を終えます。
  3. こうして、2 番目の無名関数、すなわち 2 つめのアニメーションを実行する関数は「待機」状態となります。
  4. さて、その待機関数が実行されるのは、最初のアニメーションが終わろうとする段階です。それは上の 5. の段階です。この段階では、関連づけ配列の最初の要素(つまり最初の無名関数)が削除されます。しかしここでは、既に 2 番目の animate メソッドが起動済みで、2 番目の無名関数が関連づけ配列に登録済みとなっています。従って、最初の要素が削除された後の関連づけ配列には、1つの要素=「2 番目に追加された無名関数」が存在しています。そして $.dequeue(this) メソッドは、関連づけ配列の要素数が 1 の場合にはその要素を実行します
  5. こうして、2 つめのアニメーションを引き起こす無名関数が起動され、2 つめのアニメーションが引き起こされます。このようにして、前のアニメーションが終わってから、次のアニメーションが励起されるのです。

▲ToTop

アニメーションの並行起動はどのように行われるか?【 opt.queue===false の時 】

併行実行アニメーションサンプル

本題に入る前に併行実行アニメーションのサンプルを見てみます。

併行実行アニメの一例

アニメの併行実行とは、複数のアニメーションをほぼ同時に実行することです。例えば、(1) 或るボックスを右に 200 ピクセル移動させながら、(2) 併行して文字サイズ大きくするアニメーションは以下のようになります。

但し、そもそも色は animate メソッドの操作対象外ですから、色は併行起動できません。animate メソッド内の complete メソッドを利用して逐次起動せざるを得ません。

 
併行実行アニメ sample

上の併行実行アニメーションのスクリプトコードは以下のように作成しました。(スタイルシートは上の逐次起動のものと同一であり、そちらに記載してあります。)

■ スクリプトコード
var goSidebyside = function(){
 $("#animTest2In722").animate(
  {marginLeft:"+=200px"},{queue:false,duration:2000,easing:"swing",
   complete:function(){$(this).css({backgroundColor:"darkgreen"})}})
  .animate({fontSize:"1.5em"},2000,function(){$(this).text("併行実行アニメ終了")});
}
var backSidebyside = function(){
 $("#animTest2In722")
  .animate({fontSize:"1em"},{queue:false,duration:2000,
   complete:function(){$(this).css({backgroundColor:"royalblue"})}})
  .animate({marginLeft:"-=200px"},2000,
   function(){$(this).text("併行実行アニメ sample")});
}
var startSidebyside = function(){
 $("#animTest2In722").text("併行実行アニメ sample")
  .css({backgroundColor:"royalblue",marginLeft:"100px",fontSize:"1em"});
}
var endSidebyside = function(){
 $("#animTest2In722").text("併行実行アニメ終了")
  .css({backgroundColor:"darkgreen",marginLeft:"300px",fontSize:"1.5em"});
}
$("#btn3In722").click(function(){
 if ($(this).css("margin-left")!==100+"px") startSidebyside();
 goSidebyside();
});
$("#btn4In722").click(function(){
 if ($(this).css("margin-left")!==300+"px") endSidebyside();
 backSidebyside();
});
併行実行アニメーションコードの解読

解説書には「同時起動」と言う表現もありますが、以下に見るように複数のアニメーションを全く同時に引き起こせる訳ではありません。「先行するアニメーションが終わる前に、次のアニメーションを起動する」意ですから、正確には「並行」起動というべきでしょう。

さて、queue プロパティに何も指定しなければアニメーションは 1 つずつ逐次起動されますが、並行起動はユーザーが opt.queue の値を意図的に false としない限り引き起こせません。ここに opt オブジェクトの queue プロパティ指定は次のように行います。

animate メソッドの第 2 引数 speed を { queue:false, duration:nn, easing:"xyz" } のようなオブジェクトとします。この方法によってのみ opt.queue を false に指定することが出来ます。

  1. この場合、speed メソッド内において opt.complete メソッドに登録されるメソッドは、opt.old.call(this) だけであって、$(this).dequeue() は登録されません。
    また、speedメソッド実行後に起動されるイテレータは $().queue メソッドではなく、$().each メソッドです。
  2. こうして、each メソッドが無条件で無名関数を実行しアニメーションが引き起こされます。
  3. そして当該アニメーションの最後の段階で opt.complete メソッドが起動されて、animate メソッドの第 4 引数に記述された callback 関数が実行され、アニメート終了後の何らかの操作が行われます。
  4. 次に同一インスタンスに登録された 2 つめの animate メソッドが、1 つ目の animate メソッドによるアニメーションが終わる前に起動された場合を考えます。この場合には。直上の 1. ~ 3. の過程が新しい animete メソッド内で進行します。すなわち dequeue メソッドは登録されず、たとえ先の animate メソッドが起動中であっても淡々と 2 つ目の無名関数が実行されます。こうして 1 つ目のアニメーションに並行して 2 つ目のアニメーションが引き起こされる訳です。

jquery.js におけるアニメーションコードの解読 ( 4 )

愈々、jquery アニメーションの中核をなす animate メソッドを解読します。なお、このメソッドは多くのサブルーチンを階層的に伴うので、その階層構造に見合った構成で記述します。

jQuery().animate(prop, speed, easing, callback)───その1

引数:順にアニメ用CSSオブジェクト、アニメ継続時間、easing、アニメ終了時の実行関数。但し、speed をオブジェクト形式で与える場合には第 3、第 4 引数はなし。

返値:jQueryインスタンス

機能:animateメソッドは言うまでもなく、アニメーションの中心的役割を果たすメソッドで、多くのサブルーチンを利用してアニメーションを実現します。

以下順にそのサブルーチンを見ていきます。

jQuery.speed(speed, easing, fn) クラスメソッド( 起動元は#3866 )

最初に、animate メソッドの引数をチェックし確定し、返値となるオブジェクトのプロパティを整理する jQuery.speed メソッドが登場します。このメソッドでは3つの引数の順番がどのように与えられても、そのデータ型などから内容が判断され、適正に処理されます。

引数:引数は全て animate メソッドから渡されます。

返値:オブジェクトで次の 5 つのプロパティを持ちます。 old、complete、queue、duration(= speed)、easing。この返値オブジェクトは変数 optall に代入されます。

機能:アニメーションの第 1 段階の情報整理を行います。

duration

ここにアニメーション継続時間 duration 値は 4 つのケース別に設定されます。

第 1 は jQuery.fx.off が true の時です。このときには duration 値がゼロになり、アニメーションは瞬時に履行されます。

一旦 jQuery.fx.off = true とすると、opt.duration 値がゼロになるので、同じ要素に登録されたそれ以降に起動される予定のアニメーションは、たとえ各々の継続時間がどのように指定されていようとも、全て duration 値はゼロとなり、全て瞬時に起動されます。言い換えれば全てが想定したアニメ後の状態になります。

なお、 jQuery.fx.off = true の指定は、アニメの継続時間をゼロにするのであって、アニメそのものを停止するわけではありません。アニメーションを停止させるメソッド $().stop() との差異に留意する必要があります。

jQuery.fx.off の値は引数などから与えられないので、ユーザーが外挿します。

第 2 は引数 speed が数値型の時です。このときには speed 値がそのまま opt.duration 値となります。( opt は speed メソッド内のローカル変数。以下同様)

第 3 は引数 speed が数値型でもオブジェクト型でもない場合です。この場合 speed が "slow" ならば 600 が、"fast" ならば 200 が、speed がこれらのいずれでもない場合には 400 が、それぞれ opt.duration に代入されます。(#3976~3977)

第 4 は、speed がオブジェクト型の時です。この場合には当該オブジェクトのプロパティである duration のプロパティ値が duration の値となるだけではなく、この形式で引数を指定する事によって初めて、併行起動のための指定( options.queue = false )やアニメーション途中で何かを行わせるメソッドの指定( options.step メソッド)が可能となります。

アニメ完了後に実行する関数を登録し、実行する

speed メソッドの 1 つの重要な作用として、アニメ終了後に起動するメソッドを指定する役割があります。

speed メソッドは #3980~3986 において、opt.old に animate メソッドの第 4 引数 callback を代入し、opt.complete に「 opt.queue が 未定義又は true ならば(つまりアニメを逐次起動する場合)dequeueメソッドを起動し、opt.old が関数ならばそれを実行する 」関数を登録します。

こうすることにより、opt.complete が実行されると 2 つ又は 1 つののメソッドが起動されます。dequeue メソッドが起動される場合には、当該要素に登録されている次の アニメーションが起動され、opt.old に登録されている callback( 最初のアニメーション終了後に起動するよう登録されたもの )が関数ならば、それが起動されます。

アニメを逐次起動するか、併行起動するか

更に重要なポイントがあります。それはアニメの逐次起動か、並行起動かの指定がここで行われていることです。animate メソッドの第 2 引数 speed をオブジェクト形式で与え、そのプロパティ queue をユーザーが false とします。この場合には第 3 及び第 4 引数は不要です。すると 3983 行の jQuery(this).dequeue() は opt.complete メソッドに登録されず、かつ jQuery(this).queue() ではなく jQuery(this).each() メソッドが起動されます。

これらの結果、、同一インスタンスの同一要素に登録されている別々のアニメーションの起動は相互に抑止されず、コード進行に応じて複数のアニメーションが並行的に起動されます。

他方、 opt.queue を 指定しないか true とすれば、3983 行の jQuery(this).dequeue() が opt.complete メソッドに登録され、かつ jQuery(this).each() ではなく jQuery(this).queue() メソッドが起動されます。

すると、同一インスタンスの同一要素に複数の animate メソッドが登録されている場合において、先行する アニメーション用メソッドが終わり opt.complete が呼び出され、その中にある jQuery(this).dequeue() が起動されるまでの間は、後に続く アニメーション用メソッドは起動を抑止され続けます。そして先行するアニメーション用メソッドによるアニメーションが終わり jQuery(this).dequeue() が実行されたその後に、後続するアニメーション用メソッドの内、最初に位置するそれが起動され、当該アニメーションが開始されます。

このように speed クラスメソッドと each または queue インスタンスメソッドの組み合わせによって、アニメの逐次起動か並行起動かが分岐処理されます。

▲ToTop

$().each 又は $().queueメソッド(起動元は#3868)

引数:1 つの function

返値:jQuery インスタンス

機能:このメソッドはアニメーションコード全体の 1 つの根幹的位置を占めています。インスタンスの個々の要素毎、ここのプロパティ毎に、それぞれ初期値、終了値、定期的変動値、経過時間等々を設定し、操作します。

jQuery.speed メソッドの返値を受け取った変数 optall の queue プロパティ値によって、処理が分岐されます。( なお、animate メソッドの第 2 引数 speed をオブジェクト形式で与え、そのプロパティ queue をユーザーが与えない限り queue===true に指定したことと同意になります。queue をユーザーが与えた場合には、speed メソッドの返値オブジェクト optall のプロパティに queue が設定されます。)

optall.queue プロパティが false の場合には each インスタンスメソッドが起動され、引数である唯一の関数が無条件に実行されます。他方、optall.queue プロパティが未定義か true の場合には、queue インスタンスメソッドが起動され、その時点で当該要素に関連づけられている待ち行列配列が空ならば、引数である関数が実行され、配列が空でなければ、当該関数が配列の要素に追加されます(追加された関数はこのときには実行されません)。

このようにしてアニメーションの同時並行的実行と、順次実行がコントロールされることになります。

each 並びに queue インスタンスメソッドの定義から、いずれの場合もインスタンスに登録されている各要素ノードを対象にして、巡回処理を行います。以下はこの関数の処理内容です。

▲ToTop

1. ローカル変数定義(#3870-3872)

第一引数 prop を処理するために必要なローカル変数を定義します。(1) optall から未定義プロパティを除いたプロパティを持つ opt、(2) hidden 属性の有無を登録する hidden、(3) jQuery インスタンスに格納されている要素ノードを指し示す self などが定義されます。

2. 第一引数であるpropオブジェクトの走査その1(#3875-3877)

第一引数 prop はオブジェクト形式で与えます。そのプロパティ値が hide で、変数 hidden が存在する場合(つまり当該要素は隠蔽済みとなっている)、あるいは、プロパティ値が show で変数 hidden が存在しない場合(つまり当該要素は表示済みとなっている)には、opt.complete に登録されている関数を実行します。

これは hidden 変数の値からアニメ処理が完了したと見なして、アニメ完了後のメソッドを起動するものです。

なお、opt.complete には queue 値に応じて 2 つまたは 1 つの関数が登録されていること、これによりアニメの同時並行進行か、逐次進行かが左右されることを改めて思い出すべきでしょう。(詳細は speed メソッドの項を参照)

3. 第一引数であるpropオブジェクトの走査その2(#3878-3889)

プロパティ名が height か width の場合で、当該要素が style 属性を有する場合、opt.displayプロパティに style.display 値または算出 display 値を代入し、また、style.overflow 値を opt.overflow に代入します。これらの操作は、当該要素の display プロパティと overflow プロパティの初期値を、opt オブジェクトに記憶させるもので、アニメ完了後にこれらのプロパティを初期化するときに使用します。

ここにおいて留意すべき点は、後者は styleオブジェクトのプロパティ値に限定されますが、前者は算出スタイル値の場合もあるということです。算出値と定義値との使い分けと区別は、以後にもたびたび登場しますが、animate メソッドを理解する上で重要なポイントです。

代入された opt.overflow 値が null 値でなければ、つまり overflow 値に何らかの値が定義されていれば、要素ノードの style.overflow 属性値を hidden とします。これはIE6以前のバージョンに対するバグ対策です。( 参照 → 要素のはみ出しの処理の不具合(IE/overflow):スタイルシート(CSS)辞典 - HTMLタグボード

4. prop オブジェクトの複写・待避(#3890-3891)

prop オブジェクトの未定義以外のプロパティを、opt オブジェクトのプロパティ curAnim に複写待避します。これは初期値を保存する処置であり、アニメ終了時にここで複写された値が利用されます。

▲ToTop

5. propオブジェクトをそのプロパティ毎に走査しアニメーションを描画する(#3892-3925)

jQuery インスタンスの各要素を巡回処理する中で、それぞれの要素毎にその prop オブジェクトのプロパティを巡回処理します。つまり二重の巡回処理を行うのですが、このシーンで、愈々 jquery アニメーションの肝とも言うべき jQuery.fx クラスが登場します。

5-1. jQuery.fx インスタンスの生成(#3892)

jQuery.fx クラスのインスタンス e を作り、その各プロパティを利用してアニメーションを実現しますが、まず fx クラスの定義(#4002-4009)により、次の 3 つの引数が e のプロパティとなります。

(1) e.elem = self(各要素)、(2) e.options = opt(duration、easing、old、complete、display、curAnim、及び queue の各プロパティから成るオブジェクト)、(3) e.prop = name(propオブジェクトのプロパティ名)です。

これらの 3 つのプロパティの他に、fx インスタンスには、#4013-4149 において prototype オブジェクトが定義され、update、cur、custom、show、hide、step の各メソッドが付加され、更に #4151-4171 において speeds と step というクラスプロパティも設定されます。これらについては後述しますが、同一の単語が異なる意味で使われたり、メソッド内で別のメソッドが呼び出される多重構造になっているため、jQuery.fx インスタンス e は非常にわかりにくくなっています。

5-2. propオブジェクトのプロパティ巡回の中で、その値が toggle、show または hide の場合の処理(#3895-3896)

(1) そのプロパティ値が toggle で当該要素に hidden 属性があれば、e.show(prop) メソッドが実行され、(2) そのプロパティ値が toggle で当該要素に hidden 属性がなければ、e.hide(prop) メソッドが実行され、(3)そのプロパティ値が show ならば e.show(prop)を、hideならば e.hide(prop) が実行されます。

なお、ここで登場する e インスタンスの 2 つのメソッド、e.show(prop)、e.hide(prop) については後述します。

5-3. prop オブジェクトのプロパティ巡回の中で、その値が toggle、show 及び hideでない場合の処理(#3897-3919)

このブロックでは、prop の値が数値で指定されている場合、または toggle、show、hideでも数値でもない場合(具体的にどのようなプロパティ値の場合がこれに該当するのかは解明できていません)の対応が記述されています。

まず、数値指定の場合には、算出スタイル値を変数 start に、prop オブジェクトのプロパティ値を変数 end に、それぞれ単位付きで登録します。ここで初期値と終了値が登録されます。px 以外の単位指定がされている場合には、指定された単位表現に叶う値に start 値が換算され、相対加減値指定(+= や -=)の場合にも対応しています。

こうして得られた start、end 及び unit の 3 つの引数を伴って e.custom メソッドが起動されます。

また prop の値が数値でもない何らかの値(valとする)の場合には、start( #3899 により算出スタイル値が入っているはず )、val 及び空文字をの 3つを引数として e.custom メソッドが起動されますが、具体的にどのようなプロパティがこうしたケースに該当するのか未解明です。

■ $().animate メソッド
3865: animate: function( prop, speed, easing, callback ) {
     // speed メソッドの返値 (object) を optall に代入する。
3866:  var optall = jQuery.speed(speed, easing, callback);
3867:  // queue が false ならば each メソッドを、さもなければ queue メソッドを実行
3868:  return this[ optall.queue === false ? "each" : "queue" ](function(){
3869:   // 変数定義 opt には optall のプロパティの内未定義ではないものが代入される。
3870:   var opt = jQuery.extend({}, optall), p,
       // イテレートの都度 hidden 属性の有無を hidden に登録する。
3871:    hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),
3872:    self = this;
3873: 
3874:   for ( p in prop ) { // prop オブジェクトを走査
       // プロパティ値が hide で hidden が true、または
       // プロパティ値が show で hidden が false ならば
3875:    if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
        // opt オブジェクトの complete メソッドを呼出しを返す。
3876:     return opt.complete.call(this);
3877:    // プロパティ名が height か width で style 属性があれば
3878:    if ( ( p == "height" || p == "width" ) && this.style ) {
3879:     // Store display property
        // opt.displayプロパティに display 値を代入する。
3880:     opt.display = jQuery.css(this, "display");
3881:
3882:     // Make sure that nothing sneaks out
3883:     opt.overflow = this.style.overflow; // overflow 属性値を代入する。
3884:    }
3885:   }
3886:
3887:   if ( opt.overflow != null ) // nullでなければ
3888:    this.style.overflow = "hidden"; // overflow 値を hidden とする。
3889:   // 引数 prop の未定義値でないプロパティを opt.curAnim に代入する。
3890:   opt.curAnim = jQuery.extend({}, prop);
3891:   // prop オブジェクトのプロパティ毎に走査
3892:   jQuery.each( prop, function(name, val){
       // $.fx クラスのインスタンス e を作る。
3893:    var e = new jQuery.fx( self, opt, name );
3894:    // プロパティ値に toggle、show、又は hide がある場合には
3895:    if ( /toggle|show|hide/.test(val) )
        // val が toggle で hidden が true ならば e.show(prop) を実行し、
        // val が toggle で hidden が false ならば e.hide(prop) を実行し、
        // val が toggle でないならば e.val(prop) を実行する。
3896:     e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
3897:    else { // プロパティ値に toggle、show、及び hide がない場合には
        // valを文字列に変換してから match メソッドで調べ、最初に見つかる
        // +=数値 又は -=数値 、あるいは数値を変数 parts に代入する。
3898:     var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
          // e.cur(true) 実行結果があれば、変数 start にそれを代入する。
          // ここに e.cur(true) は、jQuery インスタンスに登録されて
          // いる各要素が持つ、当該プロパティ名のタグ属性値または
          // カレントスタイル値を px 単位で取得するメソッドである。
3899:       start = e.cur(true) || 0; // なければ 0を代入する。
3900:
3901:     if ( parts ) { // parts が存在すれば
3902:      var end = parseFloat(parts[2]), // /[\d+-.]+/ の値を変数 end に 代入。
3903:        unit = parts[3] || "px"; // 単位名称を 変数 unit に代入。
3904:      // start 値を所定の単位に基づく値に変換する。
3905:      // We need to compute starting value
3906:      if ( unit != "px" ) { // 単位名が px でない場合
          // 数値と単位名を連結して end 値に対応する name スタイル値を設定する。
3907:       self.style[ name ] = (end || 1) + unit;
          // 所定単位による end値 を px 単位による end 値【e.cur(true)】で除し、
          // その比率に px 単位による start 値を乗じて所定単位の start 値を得る。
3908:       start = ((end || 1) / e.cur(true)) * start;
          // 所定単位値で name スタイル値を設定する。
3909:       self.style[ name ] = start + unit;
3910:      }
3911:
3912:      // If a +=/-= token was provided, we're doing a relative animation
3913:      if ( parts[1] ) // 相対加減値が指定されている場合には
          // start 値に end 値を加減する。
3914:       end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
3915:
3916:      e.custom( start, end, unit ); // custom メソッドを起動
3917:     } else // parts が存在しない場合
3918:      e.custom( start, val, "" ); // end 値は val で、unit はなしでcustom起動
3919:    }
3920:   }); // each メソッド終了
3921:
3922:   // For JS strict compliance
3923:   return true;
3924:  }); // #3868 の each or queue メソッド終了
3925: },

 90%近いシェアを握っているインターネットエクスプローラの描画エンジンを利用したタブbrowser。沢山のタブbrowserがあるが、多機能、カスタマイズフリー、スクリプト利用等で一日の長がある。Gekkoエンジンへの対応も行われ、IEからの自立独立の方向に向かっている。2005年7月にはIE7が登場する見通しの中で、今後の発展が望まれる。

 多様なCSS作成支援機能を備えた、タグ入力式 HTML&CSS作成支援エディタ。スキンデザインもすっきりしている。テキストエディター上で作成するよりも確実で安全にタグ打ちが出来る。
文字コードを選べないのが欠点。

 StyleNote同様のタグ入力式 HTML&CSS 作成支援エディタ。長年使用してきたが現在StyleNoteに乗り換えつつある。

 クリップボード履歴情報を活用する為のソフト。画像まで履歴を取ってくれるのが嬉しい。このソフトを使わない日は絶対ない程に重宝し、愛用している。

 起動中のウィンドウの「コピーできない」説明文などの文字列を取得し、コピー可能な文字データにするツール。何かと便利。

 ストリーミングデータを保存することが出来るソフト。動画利用には不可欠なソフトだ。

 無料ながらレイヤー機能を有し、スクリプトによる拡張も可能な、sleipnir作者が提供している優れもの画像編集ソフト。

 画面キャプチャソフトと言えばこれに勝るものなし、ではないだろうか? 様々な取得方法を有しており、ブログ作成にもHomepage作成に不可欠だ。Jtrimと並んでWoodyBellsの作品。

 複数ファイルの同時編集は出来ないが、透過pngも作れる画像編集ソフト。
(以下当該サイトから抜粋)初心者にも簡単に操作が出来るフォトレタッチソフトです。多くの加工機能で画像に様々な効果を与えることができます。非常に軽快に動作するため、ストレスなく操作できます。

 Animation Gifファイルを作れる無料ソフト。

 キャプチャソフト。画面内にサイト全体が表示しきれない場合でも、これを使えば全体をキャプチャすることが出来る。

 画像処理。画像のフォーマット変換のみならず、色数やサイズ、圧縮率の変更まで一括処理できてしまう『BatchGOO!』は、大量の画像をまとめて処理したいときに大変便利なソフト。BMP, TIFF, JPEG, PCX, PNG の相互変換をはじめ、色数・サイズ・解像度の統一、JPEG圧縮率の調節など、ホームページ用の画像や携帯電話用の壁紙を揃えるのに抜群の相性を見せる。(Vectorの当該ソフト紹介頁より抜粋引用)

 名前から直ぐに想像が付くように画像のサイズを測るためのソフトだ。Homepage作成には欠かせない。2カラム、3カラムのレイアウトを行う場合に大変重宝する。

 ランチャーソフトは沢山あるが、中でもこれが一押しだ。2年以上使ってきたがその操作性には毎日満足している。これを使い始めてからデスクトップには一切のアイコンを表示することをやめてしまった。

 AdobeReader7によって、起動時間が長すぎるという長年のユーザーの不満はある程度解消した。そのためこの高速化ソフトは存在価値が低下してしまったかもしれない。AdobeReader6迄はこのソフトによる起動高速化で恩恵を受けてきた。

 IE専用が難点だが、様々なサイト内でIDやパスワードを入力するのに重宝するソフト。コンテキストメニューから簡単に起動できるのがGood! sleipnir等のIEの描画エンジンを利用しているブラウザでも使える。

 利用しているパソコンの諸元値を取得するには、このソフトがベストだ。インストール済みソフトの一覧が取得できるのも嬉しい。

 WMPは機能が豊富なだけ重い。RealPlayerも同様だ。そこでMedia Player Classicを使いたい。動作が軽快なだけではなく、対応しているファイル形式もすこぶる多く、これひとつで、wmvもrmも表示できてしまうのだから凄い! 数多あるMedia Playerの王様と言えるだろう。

 自宅でPCを起動しているときには必ず起動しているメディアプレーヤー。何かと過剰なWinampよりも、起動も速くスキンはシンプルだ。

 DivX, Xvid, Mov, Vob, Mpeg, Mpeg4, avi, wmv, dv, などの動画をDVD-Video形式に変換できるフリーソフト。クリックするとDVD関連ソフト紹介サイト=「DVDなToolたち」なるHomepageが開きます。

----------
201002150132
201002080035
201001250128
201001242353
201001241906
201001241652
201001241413
201001130001
200910082246
200909150101
200909070032
200909012323
200908242332
200908201146
200908182132
200908161701
200908091346
200908030727
200907220708
200907212250
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。