ライブラリとかのjavascriptよーく見たら勉強になりそうだなーと思って、自分の好きな、tinyscrollingというjavascriptの中身を見てみました。
tinyscrollingというのは、するするーとスムーズにスクロールするjavascriptです。一応、このブログでも、似たようなものを使っています。ページの下方法にある、「ページの先頭へ」を押すと、するーっと上に行きます。tinyscrollingを単純に読み込んでみたサンプルはこちら。
中身を一個一個見ていったら、かなり勉強になったなぁ。丸一日かかったけど…。
間違ってる所あるかもしれないけども(特に言葉の使い方が)、ソース解説します。
解説用にTakazudoがコメント書き込みまくったjavascriptはこちら。
このHTMLサンプルとjavascriptを見ながら読んでくだされ。
tinyscrollingは、最初に挙げた、tinyscrollingを読み込んだHTMLサンプルのような動きをします。このスクリプトは、読み込むだけで、ページ内のa要素のうち、href="#pageTop"など、ページ内のアンカーポイントにジャンプルするときに、スムーズスクロールな動きをする。
このスクロールには特徴があって、飛び先との距離によって、スクロール速度が変化する。ものすごい遠い時には高速に、かなり近づいたらだんだんゆっくりにといった具合に。スピードやスクロールの減速具合のパラメーターが用意されていて、これをいじることで、好みの速度に変化させることができる。
tinyscrollingは、全ての関数、変数、スピードコントロールの設定値が、tinyScrollingオブジェクトの中にまとめられている。このtinyScrollingオブジェクトの中身は以下のような感じ。
| speed | どのくらいの速度でスクロールさせるかの設定値。小さくすればするほど早い。20とした。 |
|---|---|
| maxStep | 最高速度。(1フレームで何ピクセルスクロールさせるか) 長い距離をスクロールするときの速度になる。200とした。 |
| brakeK | 抵抗値。スクロールが終わりかけになると、徐々に減速する。その時に、どのように減速するかの値。高くすると、遠くから減速する。低くすると、ほんとに近づかないと減速しない。3とした。 |
| hash | 初期値はnull。アンカーリンクがクリックされると、リンクがhref="index.html#pageTop"だった場合、"pageTop"がここに保存される。飛び先がどこかを一時的に保存しておくための変数。 |
| currentBlock | 初期値はnull。上と同じく、飛び先の情報を格納しておくための変数。飛び先のDOMをここに一時的に格納しておく。 |
| requestedY | 初期値はnull。上と同じく、飛び先の情報を格納しておくための変数。飛び先のHTML要素の、ページ上端からの距離をここに一時的に格納しておく。 |
| init() | オンロード時に呼び出される関数。すべてのhref="#pageTop"みたいなa要素に、onclick="initScroll()"をセットする。 |
| getElementYpos(el) | 渡されたHTML要素の、ページ上端からの位置を取得し、戻り値として返す。 |
| getScrollTop() | 現在のスクロール位置を取得して、戻り値として返す。 |
| getWindowHeight() | ブラウザの表示領域の高さを返す。 |
| getDocumentHeight() | 文書の高さを取得して返す。 |
| initScroll(e) | onclick時に、スクロールを始める準備をするための関数。 |
| scroll() | 実際のスムーズスクロールは、このscroll()が短い周期で連続して実行され、ちょっとずつ表示エリアを変えていく流れになっている。このscroll()関数は、飛び先の距離と現在表示されている表示領域から、どのくらいスクロールするのかを判別して、ちょびっとスクロールさせるための関数。飛び先に到着するまで、延々この関数が実行され続ける。 |
init()でonclickをaにセット
→ クリックされたらinitScroll(e)で飛び先セット
→ scroll()で到着するまでちょっとずつスクロール繰り返し
が、基本的な流れです。
あとの細かい関数は、この流れの中で、必要に応じて呼び出されます。
んでは、1個1個関数みていくってことで。ところどころ崩して書いています。あと、このエントリは、ソースとにらめっこしていじりながら見ないと、よくわからんと思います。また、javascript勉強中なので、基本的っぽいことも含まれています。
var lnks = document.getElementsByTagName('a');
とりあえず、全てのa要素をlnksに格納します。
for(var i = 0, lnk; lnk = lnks[i]; i++) {
if (
(lnk.href && lnk.href.indexOf('#') != -1) &&
( (lnk.pathname == location.pathname) || ('/'+lnk.pathname == location.pathname) ) &&
(lnk.search == location.search)
)
{
lnk.onclick = tinyScrolling.initScroll;
}
}
ここでやっていることは、すべてのa要素の中から、href="#pgeTop"みたいなものに絞って、それらに、「クリックしたらinitScroll()実行」をセットすることです。とりあえず、一個ずつ。
(lnk.href && lnk.href.indexOf('#') != -1) &&
まず、最初に、hrefをちゃんと持ってるa要素かってのを判別。ここで、lnk.hrefって書いているけれども、この値は、具体的には、「http://yahoo.co.jp/」とか、「#pageTop」みたいな、hrefに入っている値になります。「true」じゃないとだめなんじゃないの?と自分は思ってたんですが、そーじゃないみたいですね。こういう場合って、falseに相当する値(0, -0, null, false, NaN, undefined あるいは空文字列)じゃなければ「true」って判断されるんですね。だから、ちょっと脱線するけども、
if("hoge") alert("HOGE-!");
でも、アラートほげー!がでるんですね。初めて知った。初めて知ったというか、よくわからないまま似たようなことしてた。(参考:Core JavaScript 1.5 Reference:Global Objects:Boolean - MDC)だから、この場合、hrefになんか入ってたらOKってことになる。
そして、そのhrefに、「#」って文字が入ってたらOKっていう判別になる。次。
( (lnk.pathname == location.pathname) || ('/'+lnk.pathname == location.pathname) ) &&
ここでは、リンク先が、今見ているページと同一なのかを判断してます。
リンク先のパス名は、a要素.pathnameで取得。
現在表示しているページのパス名は、location.pathnameで取得。
a要素.pathnameで、現在のパス名、を取得できるんですけど、これは、ブラウザにより挙動が多少異なるみたいです。具体的には、http://hogehoge.com/product/tv のpathnameを取得すると、「/product/tv」が返ってくるブラウザと、「product/tv」が返ってくるブラウザが存在する模様です。なんか、IEとFirefoxで違っていた。よって、「pathname」か、「"/"+pathname」が、location.pathnameと同一であれば通すという判別を行っているということですな。
(lnk.search == location.search)
最後に、クエリー(URLの?hoge=hugahugaの部分)が同一かどうかもチェックしている。これを行わないと、
http://hogehoge.com/test.php?id=tarou#contact
http://hogehoge.com/test.php?id=hanarko#contact
を同一のものとして判断してしまう。クエリーがない場合は、共に空なのでスルー
lnk.onclick = tinyScrolling.initScroll;
以上の条件を満たしたa要素(要するにhref="#pageTop"とかになっているa要素のこと)にやっとこさonclickでinitScrollをセット。
これを、すべてのa要素について繰り返すのがこの、init()です。
渡されたHTML要素の、ページ上端からの位置を取得し、戻り値として返します。
var y = 0;
while(el.offsetParent){
y += el.offsetTop
el = el.offsetParent;
} return y;
ページの上端から、HTML要素の位置を測るためには、offSetParentを使います。offSetParentを使うと、現在のHTML要素の開始位置と、その親にあたるHTML要素の開始位置がの、縦方向において、どれだけ離れているかが取得できます。
今のp → その親のdiv → その親のdiv → その親のform →… といった具合に、body要素に達するまで、親との距離を足していき、最終的にその合計がページの上端からの距離ってことになります。
現在のスクロール位置を取得して、戻り値として返します。
ページを開いたばっかりのときは0、
100pxぶん、下にスクロールしていた時は100を返すといった具合。
if(document.all) return (document.documentElement.scrollTop) ? document.documentElement.scrollTop : document.body.scrollTop;
else return window.pageYOffset;
ブラウザの実装やDOCTYPEの違いで、取得方法が変わるため、分岐している模様。詳しくは調べてないけど・・・
ブラウザの表示領域の高さを返します。
縮めてたら小さいい値が返るし、ウィンドウでかくしてたらでかい値が返る。
if (window.innerHeight) return window.innerHeight;
if(document.documentElement && document.documentElement.clientHeight) return document.documentElement.clientHeight;
ブラウザの実装やDOCTYPEの違いで、取得方法が変わるため、分岐している模様。詳しくは調べてないけど・・・
ページの高さを取得して返す。
すぐ上にあるgetWindowHeightは、ウィンドウの高さのことを言っているけども、
こちらは、ウィンドウの高さは全く関係なく、文書の高さ。
具体的には、bodyの中身を全部1つのdivの中に突っ込んだときの、そのdivの高さのようなイメージ。
if (document.height) return document.height;
if(document.body.offsetHeight) return document.body.offsetHeight;
href="#pageTop"のようなリンクがクリックされたとき、
この関数が呼び出される。(そう、それは、init()でセットされたんすよね)
どのリンクがクリックされ、どこにスクロールすればよいのかを取得し、スクロールの準備を行う。
var targ;
if (!e) var e = window.event;
if (e.target) targ = e.target;
else if (e.srcElement) targ = e.srcElement;
この4行がなんじゃこれとおもったのですが、
ここでは、クリックされたa要素をtargに格納しています。
ブラウザによって、イベントの渡され方が異なるため、こんな書き方がされてます。
「リンクがクリックされた」ということがイベントとなるんですが、
IEでは、イベントは、window.eventに格納され、
window.event.srcElementで、なにがクリックされたのかを取得できます。
これに対し、Firefoxでは、クリックされたとき、一緒にイベントオブジェクトが渡されます。
init()の中で、lnk.onclick = tinyScrolling.initScroll;と書かれていて、
何も引数が渡されていないように書かれてますけど、この時、イベントオブジェクトが渡されており、
それが、このinitScroll関数の中のe。
e.targetで、イベントの発生元を取得できる。
ということで、これでどれがクリックされたのかが分かるというわけですな。
それで、クリックされたa要素がtargに格納される、と。
参考:DOM:event:Comparison of Event Targets - MDC
tinyScrolling.hash = targ.href.substr(targ.href.indexOf('#')+1,targ.href.length);
URLの#以降(ハッシュ)を取得して、hashに格納
tinyScrolling.currentBlock = document.getElementById(tinyScrolling.hash);
currentBlockに、今取得したハッシュのIDを持つ要素(飛び先)を格納
if(!tinyScrolling.currentBlock) return;
飛び先が存在しなかったら終了(さっきでてきたtrue/falseの話)
tinyScrolling.requestedY = tinyScrolling.getElementYpos(tinyScrolling.currentBlock);
requestedYに、飛び先のY座標(ページの上端からの距離)を計算して格納
tinyScrolling.scroll();
return false;
準備がととのったところで、scroll()へ。
実際に滑らかスクロールを発生させる関数。
今、説明したばっかりのinitScrollから呼び出されます。
最初にちらっと書いたとおり、スクロールするという動作は、この関数が、speedで設定された周期で呼び出され続けて、するするスクロールするように見せています。そして、その速度が変わるのが特徴で、まぁ、とりあえず、
var top = tinyScrolling.getScrollTop();
top に、現在のスクロール位置を保存します。
次からの説明は、難しいです…ざっと読んだだけではわからんかと思います…。
この関数は、何回も繰り返して呼ばれることで、徐々に飛び先に近づいて行くんだーってことを頭においておきながら読んでください。
if(tinyScrolling.requestedY > top) {
var endDistance = Math.round((tinyScrolling.getDocumentHeight() - (top + tinyScrolling.getWindowHeight())) / tinyScrolling.brakeK);
endDistance = Math.min(Math.round((tinyScrolling.requestedY-top)/ tinyScrolling.brakeK), endDistance);
var offset = Math.max(2, Math.min(endDistance, tinyScrolling.maxStep));
}
ここで難解なものが出てくるのですが、この仕組みがtinyscrollingの最大の特徴。とりあえず、下のサンプルを開いてテキトーにスクロールさせてみてください。今は、下に向かうスクロールだけ気にして。色がちかちか変わると思います。色が変わるごとに、速度が変わります。
まず、下スクロールは、飛び先との距離が長い場合、一度にmaxStepで設定した値のピクセル数分ずつ移動します。上のサンプルで、背景がピンクになっている間は、この移動が行われています。
次に、だんだん近づいてくると、減速してきます。この、減速している間の移動が行われているとき、背景は青か紫になります。下方向への移動に際しては、ページの下端まで行く時のスクロール(ページ先頭からPoint5へ)は紫になりますが、ページの途中へ(ページ先頭からPoint1,2,3,4へ)のスクロールは、青に変化しています。この、「中くらいの距離」の移動速度は、飛び先までの距離か、ページ下端までの距離から計算されます。(移動速度といっても、次に何ピクセル移動するかという計算なので、距離と呼びます。)この距離が、maxStepを下回るとき、この、減速モードになります。
そして、この距離は、距離A(ページ下端までの距離)、距離B(飛び先の要素までの距離)のうち、値が小さい方が採用されます。各距離の計算方法はこうです。
※ 3等分されているのは、最初に設定した、設定値brakeKが3だからです。この値を変化させると、減速するタイミングが変化します。
飛び先に近づくにつれ、この距離A、Bは短くなっていきます。
飛び先がページの途中にある場合は、距離Bが、
飛び先がページの下端にあるときは、距離Aが採用されます。
この2つの距離のうちのどちらかが、maxStepで設定した数値よりも短くなったときに、その値が移動距離として採用されます。
はっきりいって、意味不明だと思いますが、まずは、飛び先がページの途中にある場合です。
この場合、距離の短い、距離Bが採用されます。距離Aは無視してください。ブラウザの表示領域は、どんどん、飛び先の要素に近づいていきます。そして、この距離Bは、飛び先に近づくにつれ、短くなっていきます。そして、1回の移動で、この距離分移動するわけですから、だんだん距離Bが短くなる…つまり、だんだんとゆっくりなスクロールするという具合になります。
次、ページの下端にスクロールする場合です。
この場合は、採用されるのは、距離Aになります。というか、この、距離Aを用意しているのは、こんな感じでページの下端にスクロールするためにあります。ページの下端には限界がありますから、距離Bだけを見ていても、距離Bはいつまでも短くならないため、減速モードに入れません。そこで、ページの下端からの距離がどんどん短くなるのに注目して、この距離Aの仕組みを作ってあるみたいです。
そして、この距離は、飛び先の要素までの距離が縮まってくると、0になってしまいます。でも、止まっちゃうのは困るので、そのくらい小さい距離になってしまったら、2px移動モードになります。このとき、背景は赤になっています。
うーんむずいなぁ…だんだん距離が縮まっていって、その距離を元に移動速度を出してるから、だんだんおそくなるよーという感じです。
今度は上へのスクロールで。
else { var offset = - Math.min(Math.abs(Math.round((tinyScrolling.requestedY-top)/ tinyScrolling.brakeK)), tinyScrolling.maxStep);
}
飛び先が上にある場合は、もうちょっと単純です。基本的な仕組みは同じです。そして今度は、要素にたどり着く前にページの上端にたどり着いてしまうことは無いので、減速モード時の計算方法は、1種類です。
飛び先に近づけば近づくほど、移動距離が縮まりますよね?
この距離がmaxStepを下回るとき、減速モード(紫背景)になり、上の移動距離計算になる。だから、だんだんおそくなるーってことで。
そして、今、ごにょごにょして出した、移動距離分を実際にスクロールさせる。
window.scrollTo(0, top + offset);
この移動が終わったら、次の判別。またscrollするか終るか。
if(Math.abs(top-tinyScrolling.requestedY) <= 1 || tinyScrolling.getScrollTop() == top) {
window.scrollTo(0, tinyScrolling.requestedY);
if(!document.all || window.opera) location.hash = tinyScrolling.hash;
tinyScrolling.hash = null;
}
飛び先(か、ページ上端)にスクロール位置合わせる。ハッシュつけられたらつける。tinyScrolling.hashをからっぽに。スクロール終了!
です。
else {
setTimeout(tinyScrolling.scroll,tinyScrolling.speed);
}
speedで設定した時間後にもういっかいscroll()です。つまりたどり着くまでずっと。
以上!
tinyscrollingにはバグがあります。自分は、いじっていて以下のようなバグに気づいた。ちょっといじればつぶせそうだけど、一応使うときは注意で。
tinyscrolling配布もとはこちら
うーん勉強になった。
This article is about... javascript , tinyscrolling
dj ssk 2009/8/02 (12:31)
最近こちらで頻繁にjsの勉強をさして頂いています。
さて、
tinyscrolling.jsの
11行目の下記の一文で使われている「 = { 」はどのような意味でしょうか?
var tinyScrolling = {
呼び名さえわからず、早速くじけました。。
Takazudo 2009/8/02 (07:59)
こんにちは、Takazudoです。
これは、名前空間というもので、tinyscrollingの全部の変数、関数をひとくくりにしてしまって、グローバル変数を汚さない工夫です。
ちょっと今ドタバタしていて全然続きが書けていないのですが、次回、ブログにエントリしようとおもっておりまーす。
dj ssk 2009/8/02 (07:18)
おー!早速、ありがとうございます。
なるほど、これが名前空間ですか。
javascriptコマンドブックを見ると、オブジェクトの部分に載ってましたね。さっき読み返してたまたま発見しました。
初めてみたので、いじって覚えてみます。