解 説

jQueryでブラウザのスクロール量を取得するには、スクロールすると発生する「scrollイベント」と「scrollTop()メソッド」を使うことで値を取得することができます。
また、ある要素の位置を動的に取得することは「offset()」を使うことで簡単に実現できます。
これらの仕組みを利用してWebページで動的な仕掛けを作成することも多いのではないでしょうか。

けれども、実際に使ってみると思わぬところで不具合が出ることも多いです。
今回はこの簡単な仕組みのはずのものが、ちょっと困った現象になる原因にスポットを当てていきます。

scrollイベント

scrollイベントはスクロールするごとにイベントが発生します。スクロールイベントは下記のように記述します。

$(window).scroll(function(){
   処理内容
})

処理内容部分に $(window).scrollTop() を記述するとスクロール量を取ることができます。
$(window).scroll(function(){
    var scrollValue = $(window).scrollTop();
})

動的に高さを取得

動的にある要素の座標を取得するには次のようにします。次の例は#no1の要素のY座標を取得しています。

$('#no1').offset().top

これで動的に指定した要素のY座標を取ることができます。例えばある要素の座標までスクロールしたら、アニメーションが起こるなどの記述ができるようになります。

ある要素までスクロールしたらアニメーションする例

スクロールしてh2要素がブラウザのブラウザの上端に近付いたら、h2要素に下線を引くサンプルを作成します。

サンプルデモ(画像はかなり重たい素材を使用しています。)
HTMLコード

<h1>Scroll Test</h1>
    <div class="img"><img src="images/welcome.jpg" alt="Welcome"></div>
    <p>Lorem ipsum...</p>
    <h2 id="no1">AAAAAA</h2>
    <div class="img"><img src="images/no1.jpg" alt="着物美人ポーズ1"></div>
    <p>Lorem ipsum...</p>
    <h2 id="no2">BBBBBB</h2>
    <div class="img"><img src="images/no2.jpg" alt="着物美人ポーズ2"></div>
    <p>Lorem ipsum...</p>
    <h2 id="no3">CCCCCC</h2>
    <div class="img"><img src="images/no3.jpg" alt="着物美人ポーズ3"></div>
    <p>Lorem ipsum...</p>
    <h2 id="no4">DDDDDD</h2>
    <div class="img"><img src="images/no4.jpg" alt="着物美人ポーズ4"></div>
    <p>Lorem ipsum...</p>
    <h2 id="no5">EEEEEE</h2>
    <div class="img"><img src="images/no5.jpg" alt="着物美人ポーズ5"></div>
    <p>Lorem ipsum...</p>

CSSコード

 h2{
    display:inline-block;
  }
  h2::after{
    content: "";
    display:block;
    height:5px;
    width:0;
    background-color: #DC0034;
    transition: 0.5s;
  }
  h2.on::after{
    width:100%;
  }

jQueryコード

$(function() {
      var no1 = $('#no1').offset().top-300;
      var no2 = $('#no2').offset().top-300;
      var no3 = $('#no3').offset().top-300;
      var no4 = $('#no4').offset().top-300;
      var no5 = $('#no5').offset().top-300;
        $(window).scroll(function(){
            var top = $(window).scrollTop();
            if(top >= no5){
              $('h2').removeClass('on');
              $('#no5').addClass('on');
            }else if(top >= no4){
              $('h2').removeClass('on');
              $('#no4').addClass('on');
            }else if(top >= no3){
              $('h2').removeClass('on');
              $('#no3').addClass('on');
            }else if(top >= no2){
              $('h2').removeClass('on');
              $('#no2').addClass('on'); 
            }else if(top >= no1){
              $('h2').removeClass('on');
              $('#no1').addClass('on'); 
            }else{
              $('h2').removeClass('on');
            }
        })
      });

サンプル概要

h2要素に擬似クラスを使って高さ5pxの下線を作っています。
ただし、初期状態ではh2要素の擬似要素のwidthは0で下線は無しの状態です。
それぞれのh2要素にはid名を付けておき、その要素までのY座標を「offset().top」で取得しています。それぞれのh2要素がブラウザ上端に来た時に赤い線がアニメーションしながら引かれる仕組みです。
このままでは線が引かれる様子が分かりにくいので、上端から300xp下の位置を取得することでその位置でアニメーションが開始するようにしています。

トリガーとしては、if文でスクロール量とそれぞれのh2座標を比較して、条件を満たせばh2要素にclass名「on」を付与するようにしています。
class名「on」に対してはCSSで擬似要素のwidthをh2要素の文字に合わせた値にして0からアニメーションする仕組みです。
尚、アニメーションはCSS3のtransitionプロパティを使いました。

if文はswitch文にすることも可能です。好みで使ってください。

//途中省略
            switch(true){
              case top >= no5 :
                $('h2').removeClass('on');
                $('#no5').addClass('on');
                break;
              case top >= no4 :
                $('h2').removeClass('on');
                $('#no4').addClass('on');
                break;
              case top >= no3 :
                $('h2').removeClass('on');
                $('#no3').addClass('on');
                break;
              case top >= no2 :
                $('h2').removeClass('on');
                $('#no2').addClass('on');
                break;
              case top >= no1 :
                $('h2').removeClass('on');
                $('#no1').addClass('on');
                break;
              default:
                $('h2').removeClass('on');
            }

これで万事うまくいくはずです。実際ローカル上のChromeでは問題なく動作します。
しかしサーバーにアップロードした途端に問題が出る場合があります。
特にSafariでは明らかに動きがおかしくなります。

トラブルの原因と解決

さて、なぜサーバーに公開したら動きがおかしくなるのでしょうか。
それはこのサンプルの場合は画像が原因しています。今回のサンプルでは問題をはっきりとさせるために画像のファイルサイズをかなり大きなものにして画像のダウンロードに時間をかけるようにしています。
つまり、画像がブラウザに読み込まれる前に、h2要素までのY座標を「offset().top」で取得してしまっているために画像の高さが正しく取得できずに不正確な座標を返してしまっているからです。特にsafariにおいては画像がダウンロードできるまでその画像の大きさは取得できていないようです。

原因がわかれば対策は簡単です。
画像が全て読み込まれてからh2要素までのY座標を「offset().top」で取得すれば良いのです。
それはloadイベントを使用します。

load()を使ったサンプルデモ
load()を使ったjQueryコード

$(function() {
      $(window).load(function() {
        var no1 = $('#no1').offset().top-300;
        var no2 = $('#no2').offset().top-300;
        var no3 = $('#no3').offset().top-300;
        var no4 = $('#no4').offset().top-300;
        var no5 = $('#no5').offset().top-300;
          $(window).scroll(function(){
              var top = $(window).scrollTop();
              if(top >= no5){
                $('h2').removeClass('on');
                $('#no5').addClass('on');
              }else if(top >= no4){
                $('h2').removeClass('on');
                $('#no4').addClass('on');
              }else if(top >= no3){
                $('h2').removeClass('on');
                $('#no3').addClass('on');
              }else if(top >= no2){
                $('h2').removeClass('on');
                $('#no2').addClass('on'); 
              }else if(top >= no1){
                $('h2').removeClass('on');
                $('#no1').addClass('on'); 
              }else{
                $('h2').removeClass('on');
              }
          })
        })
      });

これでうまくいくはずです。
が、しかしこれでもうまく動作しません。

これも嫌な落とし穴です。実はjQueryのライブラリのv3系はload()の書き方が変更になっています。もしjQueryの3系のライブラリを使用した場合はうまく動作しません。けれどもjQueryの1系のライブラリを使用していたらこれは正しく動くはずです。

3系サンプルデモ
1系サンプルデモ

jQueryの3系のライブラリを使用している場合は、on()を使用した書き方になります。
次のように記述します。
3系のon()の書き方例

$(function() {
      $(window).on("load",function() {
        var no1 = $('#no1').offset().top-300;
        var no2 = $('#no2').offset().top-300;
        var no3 = $('#no3').offset().top-300;
        var no4 = $('#no4').offset().top-300;
        var no5 = $('#no5').offset().top-300;
          $(window).scroll(function(){
              var top = $(window).scrollTop();
              if(top >= no5){
                $('h2').removeClass('on');
                $('#no5').addClass('on');
              }else if(top >= no4){
                $('h2').removeClass('on');
                $('#no4').addClass('on');
              }else if(top >= no3){
                $('h2').removeClass('on');
                $('#no3').addClass('on');
              }else if(top >= no2){
                $('h2').removeClass('on');
                $('#no2').addClass('on'); 
              }else if(top >= no1){
                $('h2').removeClass('on');
                $('#no1').addClass('on'); 
              }else{
                $('h2').removeClass('on');
              }
          })
        })
      });

この問題は画像の場合だけではなく、動画などを埋め込む時にも起こり得ます。大きなファイルサイズのものを読み込む場合はloading機能をつけるなどの工夫も必要になるでしょう。