以前「ページ内リンクのスムーススクロールのベスト(2017年9月現在)な実装を考える」という記事を書いたのですが、
この記事の中では当たり前のようにjQueryを使用していました。
アニメーションさせるならjQueryを使った方が楽、という思い込みから、jQueryを使わないという事を検討すらしていませんでした。
しかし改めて考えてみると、徐々に古いIEに対応しなくて良くなってきた昨今、最近のJavaScriptの便利なメソッドも使えるようになってきているので、実はプレーンなJavaScriptでもシンプルに実装できるんじゃないかと思い、色々と試してみました。
やはりjQueryは便利に作られているなと実感しつつも、機能を絞ればプレーンなJavaScriptでもまあまあシンプルに書ける事がわかりましたので、紹介したいと思います。
IE10以下の動作確認はしていません。
わかった事
試してみた所、アニメーションを作る時には、まずイージングが大変だという事がわかりました。
スムーススクロールのオプションとしてイージングを色々選択できるようにしようと思うと、プレーンなJavaScriptでシンプルなコードを書くのはどうしても無理があります。
逆に、イージングさえ絞ってしまえば、後は大して難しい記述は無いので、なかなかシンプルに書くことができます。
よく考えてみたら、今までスムーススクロールごときでサイト毎にイージングを変えるような事もしなかったし、
jQueryのデフォルトのイージング”swing”で、クライアント側から文句を言われたこともありません。
という事で、jQueryのデフォルトのイージング”swing”だけに限定して、コードを書いてみました。
jQueryのデフォルトのイージング”swing”限定のスムーススクロール
念のための使い方の説明ですが、下記のコードを、bodyの閉じタグの前に読み込む(<script>タグで囲むか、JSファイルに書いて読み込む)と、#から始まるページ内リンクがスムーススクロールになります。(例:<a href=”#section-1″>テキスト</a>)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var scrollElm = (function() { if('scrollingElement' in document) return document.scrollingElement; if(navigator.userAgent.indexOf('WebKit') != -1) return document.body; return document.documentElement; })(); (function() { var duration = 500; var ignore = '.noscroll'; var easing = function (t, b, c, d) { return c * (0.5 - Math.cos(t / d * Math.PI) / 2) + b; }; //jswing var smoothScrollElm = document.querySelectorAll('a[href^="#"]:not(' + ignore +')'); Array.prototype.forEach.call(smoothScrollElm, function(elm) { elm.addEventListener('click', function(e) { e.preventDefault(); var targetElm = document.querySelector(elm.getAttribute('href')); if(!targetElm) return; var targetPos = targetElm.getBoundingClientRect().top; var startTime = Date.now(); var scrollFrom = scrollElm.scrollTop; (function loop() { var currentTime = Date.now() - startTime; if(currentTime < duration) { scrollTo(0, easing(currentTime, scrollFrom, targetPos, duration)); window.requestAnimationFrame(loop); } else { scrollTo(0, targetPos + scrollFrom); } })(); }) }); })(); |
オプションとして、
7行目の
var duration = 500; は、アニメーションのスピード(ミリ秒)
8行目の
var ignore = '.noscroll' は、#から始まるリンクだけどスムーススクロールを適用させたくない要素のセレクタが指定できます。
例えば、<a href=”#hogehoge” class=”noscroll”>というタグは、スムーススクロールが無視されるようになります。
そして9行目〜の
var easing = ... ですが、
ここでjQueryのデフォルトの”swing”のイージング関数を代入しています。
一応どのイージングを使ってるかを示すために//jswingというコメントを入れていますが、
何故swingじゃなくてjswingなのかと言うと、jQueryのイージングプラグインを入れると、デフォルトのswingがjswingという名前に変わるため、それに合わせています。
この計算式は、jQueryのファイルの中に記述されているイージングの計算式を元にして作りました。
これで本当に合ってるのか実際の所はわからないのですが、検証した感じでは、実際のjQueryのイージングにかなり近い動きをしていると思います。
ちなみに、
グローバル変数を無くすために6行目から
(function() { で囲んでカプセル化しているのですが、
1行目〜の
scrollElm は、他のスクリプトでも使う可能性があるので、カプセルの外に出しています。
これも邪魔なようであれば、6行目以降に入れ込んでしまえば良いかと思います。
いつも使ってるスムーススクロールのスクリプトよりは長くなりましたが、jQueryに依存せず、このコードだけでスムーススクロールが動くというのは魅力的ではないでしょうか。
個人的には、普段のサイト制作で使用していたスムーススクロールのスクリプトとこのコードを完全に置き換える事ができます。
しかし、このままでは、もし別のイージングが必要になった時に路頭に迷ってしまいます。
結局jQueryを読み込んで、いつものスムーススクロールのスクリプトに入れ直す事になるのであれば意味がありません。
スムーススクロールのイージングを他のものに変えるという場面は相当少ないと思いますが、可能性は0では無いので、その場合の対応を少し考えてみたいと思います。
イージングを他のものに変えてみる
ひとまず、jQueryのイージングプラグインを少し編集したイージング関数の一覧を載せます。(参考:jQuery Easing Plugin (version 1.3))
イージングプラグインには無いlinerの計算式は、こちらのサイトからいただきました。→ http://gizma.com/easing/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
var easingFuncs = { linear: function (t, b, c, d) { return c * t / d + b; }, jswing: function (t, b, c, d) { return c * (0.5 - Math.cos(t / d * Math.PI) / 2) + b; }, swing: function(t, b, c, d) { return -c *(t/=d)*(t-2) + b; }, easeInQuad: function (t, b, c, d) { return c*(t/=d)*t + b; }, easeOutQuad: function (t, b, c, d) { return -c *(t/=d)*(t-2) + b; }, easeInOutQuad: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; }, easeInCubic: function (t, b, c, d) { return c*(t/=d)*t*t + b; }, easeOutCubic: function (t, b, c, d) { return c*((t=t/d-1)*t*t + 1) + b; }, easeInOutCubic: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t + b; return c/2*((t-=2)*t*t + 2) + b; }, easeInQuart: function (t, b, c, d) { return c*(t/=d)*t*t*t + b; }, easeOutQuart: function (t, b, c, d) { return -c * ((t=t/d-1)*t*t*t - 1) + b; }, easeInOutQuart: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t + b; return -c/2 * ((t-=2)*t*t*t - 2) + b; }, easeInQuint: function (t, b, c, d) { return c*(t/=d)*t*t*t*t + b; }, easeOutQuint: function (t, b, c, d) { return c*((t=t/d-1)*t*t*t*t + 1) + b; }, easeInOutQuint: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; return c/2*((t-=2)*t*t*t*t + 2) + b; }, easeInSine: function (t, b, c, d) { return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOutSine: function (t, b, c, d) { return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOutSine: function (t, b, c, d) { return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; }, easeInExpo: function (t, b, c, d) { return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOutExpo: function (t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOutExpo: function (t, b, c, d) { if (t==0) return b; if (t==d) return b+c; if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; }, easeInCirc: function (t, b, c, d) { return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; }, easeOutCirc: function (t, b, c, d) { return c * Math.sqrt(1 - (t=t/d-1)*t) + b; }, easeInOutCirc: function (t, b, c, d) { if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; }, easeInElastic: function (t, b, c, d) { 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; }, easeOutElastic: function (t, b, c, d) { 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) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; }, easeInOutElastic: function (t, b, c, d) { var s=1.70158;var p=0;var a=c; if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); if (a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; }, easeInBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; }, easeOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; }, easeInOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; }, easeInBounce: function (t, b, c, d) { return c - easingFuncs.easeOutBounce (d-t, 0, c, d) + b; }, easeOutBounce: function (t, b, c, d) { if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } }, easeInOutBounce: function (t, b, c, d) { if (t < d/2) return easingFuncs.easeInBounce (t*2, 0, c, d) * .5 + b; return easingFuncs.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b; } }; |
この中からイージング関数を選び、先程のスクリプトの9行目の var easing = function (t, b, c, d){... の中身を書き換えます。
例えば、easeOutElasticにしたい場合は、9行目を
1 2 3 4 5 6 7 |
var easing = function (t, b, c, d) { 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) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; } //easeOutElastic |
にすると、驚くほどエラスティックなスムーススクロールになります。
easeInbounceとeaseInOutBounceだけは曲者で、関数の中で別のイージング関数を使用しているため、少し工夫が必要になります。
例えばeaseInOutBounceにする場合は、下記のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var easing = function (t, b, c, d) { if (t < d/2) return easeInBounce (t*2, 0, c, d) * .5 + b; return easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b; }; //easeInOutBounce function easeInBounce(t, b, c, d) { return c - easeOutBounce (d-t, 0, c, d) + b; } function easeOutBounce(t, b, c, d) { if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } } |
非常にBounceなスムーススクロールになりました。
他にも、先程linearの計算式で参考にさせてもらった http://gizma.com/easing/ に載っている計算式を入れる事も可能です。
イージングを変更する場合は、このような方法でどうでしょうか。
easeInbounceとeaseInOutBounceの記述が少しやっかいなのが悩み所なのですが、
しかし、スムーススクロールのイージングでこういう奇抜なイージングを使う可能性は限りなく0に近い気がします。
やはり考えれば考えるほど、最初に紹介したjswingだけのスクリプトで事足りる気がします。
ただ、もしかしたら、スムーススクロールごときでも毎回イージングを検討したいという人もいるかもしれません。
そういう方は、下記のように全てのイージング関数を予め用意しておき、使用するイージングが決まったら他の関数を消す、という方法ではどうでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
var scrollElm = (function() { if('scrollingElement' in document) return document.scrollingElement; if(navigator.userAgent.indexOf('WebKit') != -1) return document.body; return document.documentElement; })(); (function() { var duration = 500; var ignore = '.noscroll'; var easing = 'easeInOutBounce'; var easingFuncs = { linear: function (t, b, c, d) { return c * t / d + b; }, jswing: function (t, b, c, d) { return c * (0.5 - Math.cos(t / d * Math.PI) / 2) + b; }, swing: function(t, b, c, d) { return -c *(t/=d)*(t-2) + b; }, easeInQuad: function (t, b, c, d) { return c*(t/=d)*t + b; }, easeOutQuad: function (t, b, c, d) { return -c *(t/=d)*(t-2) + b; }, easeInOutQuad: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; }, easeInCubic: function (t, b, c, d) { return c*(t/=d)*t*t + b; }, easeOutCubic: function (t, b, c, d) { return c*((t=t/d-1)*t*t + 1) + b; }, easeInOutCubic: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t + b; return c/2*((t-=2)*t*t + 2) + b; }, easeInQuart: function (t, b, c, d) { return c*(t/=d)*t*t*t + b; }, easeOutQuart: function (t, b, c, d) { return -c * ((t=t/d-1)*t*t*t - 1) + b; }, easeInOutQuart: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t + b; return -c/2 * ((t-=2)*t*t*t - 2) + b; }, easeInQuint: function (t, b, c, d) { return c*(t/=d)*t*t*t*t + b; }, easeOutQuint: function (t, b, c, d) { return c*((t=t/d-1)*t*t*t*t + 1) + b; }, easeInOutQuint: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; return c/2*((t-=2)*t*t*t*t + 2) + b; }, easeInSine: function (t, b, c, d) { return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOutSine: function (t, b, c, d) { return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOutSine: function (t, b, c, d) { return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; }, easeInExpo: function (t, b, c, d) { return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOutExpo: function (t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOutExpo: function (t, b, c, d) { if (t==0) return b; if (t==d) return b+c; if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; }, easeInCirc: function (t, b, c, d) { return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; }, easeOutCirc: function (t, b, c, d) { return c * Math.sqrt(1 - (t=t/d-1)*t) + b; }, easeInOutCirc: function (t, b, c, d) { if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; }, easeInElastic: function (t, b, c, d) { 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; }, easeOutElastic: function (t, b, c, d) { 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) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; }, easeInOutElastic: function (t, b, c, d) { var s=1.70158;var p=0;var a=c; if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); if (a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; }, easeInBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; }, easeOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; }, easeInOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; }, easeInBounce: function (t, b, c, d) { return c - easingFuncs.easeOutBounce (d-t, 0, c, d) + b; }, easeOutBounce: function (t, b, c, d) { if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } }, easeInOutBounce: function (t, b, c, d) { if (t < d/2) return easingFuncs.easeInBounce (t*2, 0, c, d) * .5 + b; return easingFuncs.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b; } }; var smoothScrollElm = document.querySelectorAll('a[href^="#"]:not(' + ignore +')'); Array.prototype.forEach.call(smoothScrollElm, function(elm) { elm.addEventListener('click', function(e) { e.preventDefault(); var targetElm = document.querySelector(elm.getAttribute('href')); if(!targetElm) return; var targetPos = targetElm.getBoundingClientRect().top; var startTime = Date.now(); var scrollFrom = scrollElm.scrollTop; (function loop() { var currentTime = Date.now() - startTime; if(currentTime < duration) { scrollTo(0, easingFuncs[easing](currentTime, scrollFrom, targetPos, duration)); window.requestAnimationFrame(loop); } else { scrollTo(0, targetPos + scrollFrom); } })(); }) }); })(); |
別に使わないのをわざわざ消さなくてもいいんじゃないかという意見もあると思いますが、
スムーススクロールでここまでの長いコードは必要ないというか、
もし他の人から渡された案件でこのコードが入ってたら、多分腹立つと思います。
という事で、
個人的なスムーススクロールのベスト(2018年1月現在)は、最初に載せたコードで良いんじゃないかなと思っています。
One thought to “【JavaScript】スムーススクロールをjQueryを使わずにシンプルに実装する【Vanilla JS】”