上下左右にスクロールするスクリプト

/web/javascript

Note: この記事は、3年以上前に書かれています。Webの進化は速い!情報の正確性は自己責任で判断してください。

ライブラリを使わない自作JSシリーズ、今回はスクロール処理です。いわゆる「ページトップへ」。特に珍しくもない動作ですが、JSでアニメーション処理してみたいって人の取っ掛かりとしてはほど良い課題だと思います。

今回のテーマは『高速化』。前々から当サイトでも密やかに使ってますが、一部環境でもっさりした動作になってしまっていて、高速化が課題でした。書き方も古かったし。原因追求が面倒でしばらく投げてましたが、offsetWidthは重いという情報をゲットしまして、「おぉ、原因これじゃん!」と一念発起して組み上げた次第です。伏して待て。忘れるな。機会は密やかに訪れる ...てとこでしょうか。この際だからとオマケで追加した機能が以下。

  1. 横方向への移動
  2. スクロール限界を考慮した終了処理
  3. keydown, mousedown, mousewheelでの動作キャンセル

いざ組みあがってみると、けっこう満足していたり。単純な縦移動ならいっぱいありますが、横移動できるのってあんまり見たことないですから、これはけっこう貴重かもしれない。移動中に目を引くコンテンツがあったり、速度が遅くて「イライラするっ 手動で移動させろ!」て場合とかに、動作キャンセルも意外と重宝するかと思います。

サンプルはこちら。解説は以下に。

HTML

  1. <a href="#top" onClick="scroll.leap('top');">↑Topへ移動します</a>

起動部分はスタンダードなonclickイベントで。<a>要素への付与(ページ内リンク抽出)はやってません。無駄だから

サイト全体で考えるとページ内リンクの頻度は低いので、その為に「前ページ全ノード一斉走査 → 該当要素に付与」ってのは積み重ってパフォーマンスを落とすデメリットのほうが大きいので、この方が良いでしょう。そういえば、偉い人も「ページ内リンクは使っちゃダメよー」と言ってます。

JavaScript

  1. var scroll = {
  2. leap: function(idname){
  3. var start = this.getScrollPos();
  4. var obj = $(idname);
  5. var goal = this.getElementPos(obj);
  6.  
  7. var moveY = goal.y - start.y;
  8. var moveX = goal.x - start.x;
  9.  
  10. var direction = new Object;
  11. direction.x = moveX > 0 ? 'plus' : 'minus'
  12. direction.y = moveY > 0 ? 'plus' : 'minus'
  13.  
  14. var maxScroll = this.getMaxScroll();
  15. goal.x = goal.x > maxScroll.x ? maxScroll.x : goal.x;
  16. goal.y = goal.y > maxScroll.y ? maxScroll.y : goal.y;
  17.  
  18. this.land(goal,direction);
  19. this.cancel();
  20. },
  21.  
  22. land: function(goal,direction){
  23. var pos = this.getScrollPos();
  24. var arrive = new Object;
  25. arrive.x = direction.x == 'plus'
  26. ? Math.floor(pos.x + ((goal.x - pos.x) / 5)) + 1
  27. : arrive.x = Math.max(Math.floor(pos.x - (pos.x / 6)),goal.x);
  28. arrive.y = direction.y == 'plus'
  29. ? Math.floor(pos.y + ((goal.y - pos.y) / 5)) + 1
  30. : arrive.y = Math.max(Math.floor(pos.y - (pos.y / 6)),goal.y)
  31.  
  32. scrollTo(arrive.x,arrive.y);
  33.  
  34. if(direction.y == 'plus' && direction.x == 'plus') {
  35. if(arrive.y < goal.y || arrive.x < goal.x){
  36. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  37. }
  38. }
  39. else if(direction.y == 'plus' && direction.x == 'minus') {
  40. if(arrive.y < goal.y || arrive.x != goal.x){
  41. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  42. }
  43. }
  44. else if(direction.y == 'minus' && direction.x == 'plus') {
  45. if(arrive.y != goal.y || arrive.x < goal.x){
  46. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  47. }
  48. }
  49. else if(direction.y == 'minus' && direction.x == 'minus') {
  50. if(arrive.y != goal.y || arrive.x != goal.x){
  51. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  52. }
  53. }
  54. else {
  55. console.log('error!');
  56. }
  57. },
  58.  
  59. cancel: function(){
  60. addEvent(document,'mousedown',function(){clearTimeout(scroll.timerID)},false);
  61. addEvent(document,'keydown',function(){clearTimeout(scroll.timerID)},false);
  62. addEvent(document,'mousewheel',function(){clearTimeout(scroll.timerID)},false);
  63. addEvent(document,'DOMMouseScroll',function(){clearTimeout(scroll.timerID)},false);
  64. },
  65.  
  66. getMaxScroll: function(){
  67. var scrSize = this.getScreenSize();
  68. var docSize = this.getDocSize();
  69.  
  70. var maxScroll = new Object;
  71. maxScroll.x = docSize.x - scrSize.x;
  72. maxScroll.y = docSize.y - scrSize.y;
  73.  
  74. return maxScroll;
  75. },
  76.  
  77. getElementPos: function(elem){
  78. var obj = new Object();
  79. obj.x = elem.offsetLeft;
  80. obj.y = elem.offsetTop;
  81.  
  82. while(elem.offsetParent) {
  83. elem = elem.offsetParent;
  84. obj.x += elem.offsetLeft;
  85. obj.y += elem.offsetTop;
  86. }
  87. return obj;
  88. },
  89.  
  90. getScrollPos: function(){
  91. ... goodness ...
  92. },
  93.  
  94. getDocSize: function(){
  95. ... goodness ...
  96. },
  97.  
  98. getScreenSize: function() {
  99. ... goodness ...
  100. }
  101. }

まずは全体をざっくりと。scroll.leap()が起動部分。scroll.land()が処理部分。scroll.cancel()が動作キャンセルのイベント割当。その他のメソッドで、現在位置やドキュメント・サイズなんかを取得してます。この部分については前に書いた記事で詳しく書いてます。

以下、要点解説

高速化

  1. 位置取得は1回だけ
  2. 単純なIF分岐は三項演算子で
  3. 重複する処理はなるべく外に出す

高速化!とは言っても、意識したのはこれくらいです。特に珍しいことはしていない。現在のスクロール位置だけは毎回取得してますが、頭こんがらがってきたのでまーいーかー、ぐらいには緩々。

三項演算子は「<条件式> ? <真の場合の処理> : <偽の場合の処理」みたいな組み込み演算子ですが、これはIF分より速いです。可読性低いので扱いにくいですが、単純な分岐なら積極的に使うべきですね。

3番目は位置取得系メソッドとかですね。ホントはscrollオブジェクトからも出して共通部分に移すべきなんでしょうが、今回は単体でサンプルとするために組み入れてます。まあこのままでもscroll.getElementPos()とでもすれば、何処からでも参照できるけど。

実行準備

  1. leap: function(idname){
  2. var start = this.getScrollPos();
  3. var obj = $(idname);
  4. var goal = this.getElementPos(obj);
  5.  
  6. var moveY = goal.y - start.y;
  7. var moveX = goal.x - start.x;
  8.  
  9. var direction = new Object;
  10. direction.x = moveX > 0 ? 'plus' : 'minus'
  11. direction.y = moveY > 0 ? 'plus' : 'minus'
  12.  
  13. var maxScroll = this.getMaxScroll();
  14. goal.x = goal.x > maxScroll.x ? maxScroll.x : goal.x;
  15. goal.y = goal.y > maxScroll.y ? maxScroll.y : goal.y;
  16.  
  17. this.land(goal,direction);
  18. this.cancel();
  19. },

2~4行目で現在位置および目標位置の取得。3行目は良くある$()関数です。「return document.getElementById(obj)」してるだけ。

6~11行目にて、移動方向が正(↓→)か負(←↑)かを出してます。正か負かで移動速度の計算式が変わってきますから、ここはけっこう大事。

13行目でスクロール限界値を取得し、14~15行目で目標位置の再設定をしてます。ここ注意。スクロールの終点は要素の左上を目標に取得しているのですが、要素に十分な大きさと余白がない場合、ドキュメントの右か下の端に引っ掛ってこれ以上スクロールできない状態に陥ります。そのままだと目標地点までスクロールできずに処理が無限ループに陥るため、限界位置を目標値だと上書きして回避するわけです。この部分を削る形でカスタムする場合は、特に気を付けてくださいね。

スクロール処理

  1. land: function(goal,direction){
  2. var pos = this.getScrollPos();
  3. var arrive = new Object;
  4. arrive.x = direction.x == 'plus'
  5. ? Math.floor(pos.x + ((goal.x - pos.x) / 5)) + 1
  6. : arrive.x = Math.max(Math.floor(pos.x - (pos.x / 6)),goal.x);
  7. arrive.y = direction.y == 'plus'
  8. ? Math.floor(pos.y + ((goal.y - pos.y) / 5)) + 1
  9. : arrive.y = Math.max(Math.floor(pos.y - (pos.y / 6)),goal.y)
  10.  
  11. scrollTo(arrive.x,arrive.y);
  12.  
  13. if(direction.y == 'plus' && direction.x == 'plus') {
  14. if(arrive.y < goal.y || arrive.x < goal.x){
  15. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  16. }
  17. }
  18. else if(direction.y == 'plus' && direction.x == 'minus') {
  19. if(arrive.y < goal.y || arrive.x != goal.x){
  20. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  21. }
  22. }
  23. else if(direction.y == 'minus' && direction.x == 'plus') {
  24. if(arrive.y != goal.y || arrive.x < goal.x){
  25. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  26. }
  27. }
  28. else if(direction.y == 'minus' && direction.x == 'minus') {
  29. if(arrive.y != goal.y || arrive.x != goal.x){
  30. this.timerID = setTimeout( function(){ scroll.land(goal,direction) },10);
  31. }
  32. }
  33. else {
  34. console.log('error!');
  35. }
  36. },

この部分はループです。終点にスクロールが完了するまでの間、繰り返し実行されます。

3~9行目。arriveオブジェクトに、今回の移動距離を計算して格納します。?で始まる行が正の移動(→↓)、:で始まる行が負の移動(↑←)に対応する計算式になります。除算の数字(この場合は5と6)を増やせば速く、減らせば遅くなります。もちろん自作の式を入れても良いですが、ここだけでもある程度は速度調節できます。

そうして得られた結果をもとに、11行目でスクロール。スクロールを実行するメソッドには上記のscrollTo()の他に、相対位置を指定するscrollBy()がありますが、後者だと移動先が起動時のスクロール位置に影響されるため、今回は使用していません。この辺は好みもあるでしょう。

13行目以降は、次回のループの実行条件です。この部分は巧い簡略化が見つからなかった。次の課題ですね。とりあえず縦移動のみなら分岐を半分に減らせます。タイムアウトの「this.timerID」への代入は、キャンセル時のclearTimeout()指定に必要。

キャンセル処理

  1. cancel: function(){
  2. addEvent(document,'mousedown',function(){clearTimeout(scroll.timerID)},false);
  3. addEvent(document,'keydown',function(){clearTimeout(scroll.timerID)},false);
  4. addEvent(document,'mousewheel',function(){clearTimeout(scroll.timerID)},false);
  5. addEvent(document,'DOMMouseScroll',function(){clearTimeout(scroll.timerID)},false);
  6. },

これは簡単ですね。任意のイベントにclearTimeout()を割り当てているだけです。addEvent()関数は組み込みではありませんが、有名なので以前の記事で触れてます。

memo: Firefox用に「DOMMouseScroll」イベントを追加。

どないでしょー? いちお EeePC 4G-X とか CPU:800/Mem:256MBなXPでも確認してますので、そうそう重いことはないかと思います。(検証に協力してくれた某W氏およびhamashun先生に感謝!

これを機会にって訳でもないけど、日本でも横スクロールなWebサイトが流行るといいですね。スクリプト側でボックスずらせば、JS無効環境でも問題なしさ!

関連記事

  1. 動作サンプル
  2. イベントオブジェクトまとめ
  3. ページサイズやスクロール位置を取得する

Note: スパム対策が面倒なので、コメント投稿を廃止しました。以前のコメントは残します。
ご意見・ご要望はtwitter@sigwygかはてブコメントにて。