iOSやAndroidなどのスマホの画面は基本的に慣性が効くようになっています。
(スワイプして指を離すとすべるように画面が移動するアレ)
そのため、逆に慣性をオフにした場合にとても違和感があり、また慣性の効き方がおかしいとクオリティがとても下がったように感じます。
Webサイトでiframeを使ったりしていると慣性が効かない部分がたまにあることに気づく人もいるでしょう。
また、スマホゲームなどではUI部分は独自実装(やUnityなどのフレームワークが準備してくれたUI)されているものが多く、OSの動きと比べると違和感を覚えることも少なくありません。
過去に、iOSのスクロールを真似たカスタムスクロールのビューを作ってみたことがありますが、やはり違和感を消すことはできませんでした。
普段ユーザーが当たり前に使う部分だけに、慣性を活かしたコンテンツを作りたいものです。
と、前置きが長くなりましたが、この慣性スクロール(や慣性を利用した動き)について記事にしたことがなかったので、自分のメモとしても書いておこうと思います。
サンプル
今回の記事のために簡単なサンプルを作成しました。
画面に表示されているボール(?)をドラッグして離すと、ドラッグしたスピードに応じて動くように作ってあります。
(反射したほうが面白いかなと思ってやっているだけなので、そのあたりの挙動が微妙におかしいのはご愛嬌w)
実装
ドラッグ中は基本的に、現在のマウス位置を補足してDOMの位置を移動するだけなので特に難しいことはしていません。
重要なのは、ドラッグをやめた(離した)ときにどう処理するか、です。
これを実現しているのが「速度(Velocity)」の差分の加算です。
ドラッグ中に逐一速度を計算し、それを加算していきます。
var now = Date.now();
var deltaTime = now - this._prevTime;
var eventPos = this._positionizeByEvent(evt);
var deltaPosition = Vector2.sub(eventPos, this._prevPosition);
var velocity = Vector2.divisionScalar(deltaPosition, (deltaTime || (deltaTime = 1)));
var deltaVelocity = Vector2.sub(velocity, this._prevVelocity);
this._velocity.add(deltaVelocity);
// ドラッグ中は差分位置を自身の`position`に加算して補正し続ける
this._position = Vector2.add(this._position, deltaPosition);
this._prevTime = now;
this._prevPosition = eventPos;
this._prevVelocity = velocity;
例えば同じ方向にすばやく動かされた場合は早くなるように、また逆方向に動かされたらその分減速するように計算します。
そしてドラッグをやめた瞬間に、それまで計算した速度を利用してDOMを移動させるわけです。
当然、速度をそのまま使っているとどこまでも飛んでいってしまうので(宇宙空間のように)、ある程度減速するように計算ごとに速度を減らしていきます。
この減らす部分は感覚的にこれくらいかな、というのを計算に利用しているだけで、特に根拠のある計算ではありません。が、それなりに慣性が効いているように見えます。
_dampingVelocity: function () {
// `this.damping`はおよそ10
var damping = Vector2.divisionScalar(this._velocity, this.damping);
this._velocity.sub(damping);
// 現在の速度が十分小さくなったら停止と見なす
if (this._velocity.lessThen(0.001)) {
this._velocity = Vector2.zero;
}
}
やっていることの全体像としては、ユーザーのドラッグ位置の差分から速度を計算し、さらに速度の差分を加算、マウスを離したときにそれまで計算してきた速度に係数を利用し、一定の減衰率で減速する、というものです。
実際に速度を減衰させて位置を計算しているのは以下のコードになります。
_dragRelease: function () {
var _this = this;
var zero = Vector2.zero;
var past = Date.now();
(function loop() {
_this._dampingVelocity();
var now = Date.now();
var delta = now - past;
_this._position = Vector2.add(_this._position, Vector2.multiplyScalar(_this._velocity, delta));
// 中略
if (_this._velocity.equal(zero)) {
_this._reset();
_this._stop();
return;
}
past = now;
_this._loopTimer = setTimeout(loop, 16);
}());
}
上記はmouseup
時に呼ばれるメソッドです。
var zero
は停止判定に使うためのもので、負荷軽減のために予めインスタンスを生成しているだけです。
_this._position = Vector2.add(_this._position, Vector2.multiplyScalar(_this._velocity, delta));
速度を減衰させたのち、ループ内でかかった時間を測定してその時間分掛けてやります。(速度はpx/ms
なので、経過した時間だけ掛ける)
あとはループを回すたびに徐々に速度が低下し、いずれ停止する、というわけです。
MathJS
ちなみに、今回はVetctor2
という専用のクラスを作って計算をしていますが、こうした数学的な計算を行うことはよくあるので、MathJSというライブラリを自作しています。
本ライブラリはベクトルや行列を扱うためのライブラリです。
WebGLを意識して作ったので、Syntaxなどはだいぶ似せてます。
もしよかったら使ってみて、使い勝手などをFBしてくれるとうれしいです(ㆆᴗㆆ)
まとめ
慣性など、物理を取り入れるととても質感が向上します。
特にスマホコンテンツなどは触っていて気持ちのいいものになります。
ぜひ取り入れて、よりよいユーザー体験を作りたいですね。