Takazudo Clipping*

  • 文字サイズ小
  • 文字サイズ中
  • 文字サイズ大

tinyscrolling解析

ライブラリとかのjavascriptよーく見たら勉強になりそうだなーと思って、自分の好きな、tinyscrollingというjavascriptの中身を見てみました。

tinyscrollingというのは、するするーとスムーズにスクロールするjavascriptです。一応、このブログでも、似たようなものを使っています。ページの下方法にある、「ページの先頭へ」を押すと、するーっと上に行きます。tinyscrollingを単純に読み込んでみたサンプルはこちら。

中身を一個一個見ていったら、かなり勉強になったなぁ。丸一日かかったけど…。
間違ってる所あるかもしれないけども(特に言葉の使い方が)、ソース解説します。
解説用にTakazudoがコメント書き込みまくったjavascriptはこちら。
このHTMLサンプルとjavascriptを見ながら読んでくだされ。

tinyscrollingの特徴

tinyscrollingは、最初に挙げた、tinyscrollingを読み込んだHTMLサンプルのような動きをします。このスクリプトは、読み込むだけで、ページ内のa要素のうち、href="#pageTop"など、ページ内のアンカーポイントにジャンプルするときに、スムーズスクロールな動きをする。

このスクロールには特徴があって、飛び先との距離によって、スクロール速度が変化する。ものすごい遠い時には高速に、かなり近づいたらだんだんゆっくりにといった具合に。スピードやスクロールの減速具合のパラメーターが用意されていて、これをいじることで、好みの速度に変化させることができる。

tinyscrollingの大まかな構造

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勉強中なので、基本的っぽいことも含まれています。

init()

とりあえずaとってくる

var lnks = document.getElementsByTagName('a');

とりあえず、全てのa要素をlnksに格納します。

href="#pageTop"みたいなのにイベントセット

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()です。

getElementYpos(el)

渡された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要素に達するまで、親との距離を足していき、最終的にその合計がページの上端からの距離ってことになります。

getScrollTop()

現在のスクロール位置を取得して、戻り値として返します。
ページを開いたばっかりのときは0、
100pxぶん、下にスクロールしていた時は100を返すといった具合。

if(document.all) return (document.documentElement.scrollTop) ? document.documentElement.scrollTop : document.body.scrollTop;
else return window.pageYOffset;   

ブラウザの実装やDOCTYPEの違いで、取得方法が変わるため、分岐している模様。詳しくは調べてないけど・・・

getWindowHeight()

ブラウザの表示領域の高さを返します。
縮めてたら小さいい値が返るし、ウィンドウでかくしてたらでかい値が返る。

if (window.innerHeight)    return window.innerHeight;
if(document.documentElement && document.documentElement.clientHeight) return document.documentElement.clientHeight;

ブラウザの実装やDOCTYPEの違いで、取得方法が変わるため、分岐している模様。詳しくは調べてないけど・・・

getDocumentHeight()

ページの高さを取得して返す。
すぐ上にあるgetWindowHeightは、ウィンドウの高さのことを言っているけども、
こちらは、ウィンドウの高さは全く関係なく、文書の高さ。
具体的には、bodyの中身を全部1つのdivの中に突っ込んだときの、そのdivの高さのようなイメージ。

if (document.height) return document.height;
if(document.body.offsetHeight) return document.body.offsetHeight;

initScroll(e)

href="#pageTop"のようなリンクがクリックされたとき、
この関数が呼び出される。(そう、それは、init()でセットされたんすよね)
どのリンクがクリックされ、どこにスクロールすればよいのかを取得し、スクロールの準備を行う。

イベントからクリックされたa要素を取得

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()へ。

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にはバグがあります。自分は、いじっていて以下のようなバグに気づいた。ちょっといじればつぶせそうだけど、一応使うときは注意で。

  • IEの互換モードでは、下方向のスクロールに対してtinyscrollingが動作しない。
  • a要素の中にimgがあるとjavascriptエラー。
  • html,body{height:100%} などとしていると、下方向のスクロールがうまく動作しないっぽい。

tinyscrolling配布元

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コマンドブックを見ると、オブジェクトの部分に載ってましたね。さっき読み返してたまたま発見しました。
初めてみたので、いじって覚えてみます。


  • コメントを書く
: 
: 
: 
TrackBack URL (この記事についてトラックバックしたい時は以下のURLを指定してください)
http://gyauza.egoism.jp/cgi/mt/mt-tb.cgi/1781



2007 © Takazduo Some Rights Reserved.