スクロール連動アニメーションが捗るかもしれないクラスを作ってみた
作成したクラスで可能なこと
- 動かしたい要素を指定
- 動かしたい要素が、画面内のどこにきたらアニメーションを開始するか指定
- スクロール量を取得し、任意の数値へ変換(正規化して線形補完)
- アニメーションにイージングを効かせる
改善したいところ
- 変化させたい数値があるたびに、インスタンス化する必要がある
- 一度インスタンス化すると永遠にスクロールイベントが呼ばれ続けることになるのでパフォーマンスが心配
作成したクラスと使い方(コメントアウト)
class ScrollFunction {
constructor(target, scrollMax, rootMargin, ease, min, max) {
this.target = target; //変化させたいターゲット
this.scrollMax = scrollMax; //任意のスクロール量の最大値
this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
this.ease = ease; //イージング関数を入れる
this.min = min; //任意の範囲の最小値
this.max = max; //任意の範囲の最大値
this.sn = new GetScrollNum();
this.sn.getResult();//スクロール量取得用関数を実行
this.scrollY = this.sn.scrollY; //画面上部からのスクロール量
this.targetPos = this.target.getBoundingClientRect().top + this.scrollY; //targetのページ上部からの位置
this.targetScroll =
this.scrollY - this.targetPos + innerHeight + this.rootMargin; ///任意の要素がrootMarginの位置に来たときにスクロール量を0としてscrollMaxまで変化する
this.resultNormal = 0; //linearでmin~maxの間を変化する変数
this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
this._getResultNum();//スクロール量を取得して値を更新する
}
//正規化
_norm(v, a, b) {
return (v - a) / (b - a);
}
//線形補完
_lerp(a, b, t) {
return a + (b - a) * t;
}
_getResultNum() {
//要素が画面内に最初からいる時(FV内にある時)スクロール量をそのまま正規化に使用する
if (this.targetPos < innerHeight + this.target.clientHeight) {
window.addEventListener('scroll', function () {
this.sn.getResult();
this.scrollY = this.sn.scrollY;
if (this.scrollY > this.scrollMax) {
this.scrollY = this.scrollMax;
}
const normalizeNum = this._norm(this.scrollY, 0, this.scrollMax); //0~1
const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
}.bind(this));
} else {
window.addEventListener(
"scroll",
function () {
this.sn.getResult();
this.scrollY = this.sn.scrollY;
this.targetScroll =
this.scrollY - this.targetPos + innerHeight + this.rootMargin;
//要素が画面内に入ってからのスクロール量を0~this.scrollMaxに留める
if (this.targetScroll !== undefined && this.targetScroll < 0) {
this.targetScroll = 0;
} else if (
this.targetScroll !== undefined &&
this.targetScroll > this.scrollMax
) {
this.targetScroll = this.scrollMax;
}
const normalizeNum = this._norm(this.targetScroll, 0, this.scrollMax); //0~1
const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
}.bind(this)
);
}
}
}
//使い方
//target(第一引数)にはElementを入れる = document.getElementByID('任意の要素');
//イベント外でインスタンス化
//スクロールイベント内で欲しい値を取得(resultEase or resultNormal)
//targetの変更したいスタイルに取得した数値を適応する
クラス内での処理の流れ
コンストラクタ内での処理
- クラス内変数に引数を格納
- GetScrollNumクラスをインスタンス化
- GetScrollNumクラスのgetResultメソッドを実行
- スクロール量を取得
- targetのページ上部からの位置を変数に格納
- targetが任意の位置(rootMargin)に来たときにスクロール量を0とする変数「this.targetScroll」を定義
- クラス内メソッドgetScrollNumを実行
_normについて
正規化用関数です。
正規化とは、ある範囲の値を0~1の値に変換することです。
例:スクロール量 100px ~ 500px を 100pxの時0,500pxの時1として 0~1の値に変換する
_lerpについて
線形補完用関数です。
ちょっとこれは、僕もとても理解が浅いのですが
正規化した値を、tに入れると任意の範囲(a~b)の中の対応する位置を返す関数と思っています。
例:正規化した値(0~1で変化する)が0.5の時
線形補完関数にa:100,b,300を入れていた場合、戻り値は200になります。(100~300の0.5の位置)
_getResultNumについて
スクロールイベントの中で、スクロール量、targetScrollの値を更新しています。
要素が最初から画面内にいるとき(FV内にある時)とそうでないときで、正規化に使用する値を変えています。
そして、条件式を設けてスクロール量を0~scrollMaxの間に留めるようにしています。
norm関数を用いてthis.targetScrollを正規化し変数normalizeNumに格納
イージング関数に正規化した数値を引数として渡して、イージングが効いて正規化された変数:easeNumを定義
それぞれを用いて、線形補完。
そのままではthisがwindowオブジェクトを指してしまうため、bindする。
活用例とコード
活用したクラスとイージング関数も一緒に載せています。
//----------------------------------------------------------------
//スクロール量、率、正規化した値を取得するクラス
//インスタンス化して、スクロールイベント内でgetResultを実行する。
class GetScrollNum {
constructor() {
this.scrollY = "";
this.normalizeScrollY = "";
this.scrollRate = "";
this.viewH = document.documentElement.scrollHeight;
this.totalScrollY = this.viewH - innerHeight;
}
getResult() {
this.scrollY = window.pageYOffset; //スクロール量
this.normalizeScrollY = this.scrollY / this.totalScrollY; //スクロール量正規化
this.scrollRate = Math.floor(this.normalizeScrollY * 100); //スクロール率
}
}
//使い方
//スクロールイベント内でインスタンス化
//欲しい値を引っ張り出す
//----------------------------------------------------------------
//------------------------------------------------
//イージング関数
function easeInOutQuint(x) {
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
}
function easeInQuint(x) {
return x * x * x * x * x;
}
function easeInOutBack(x) {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return x < 0.5
? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
: (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
};
//------------------------------------------------
//---------------------------------------------------------------
class ScrollFunction {
constructor(target, scrollMax, rootMargin, ease, min, max) {
this.target = target; //変化させたいターゲット
this.scrollMax = scrollMax; //任意のスクロール量の最大値
this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
this.ease = ease; //イージング関数を入れる
this.min = min; //任意の範囲の最小値
this.max = max; //任意の範囲の最大値
this.sn = new GetScrollNum();
this.sn.getResult();//スクロール量取得用関数を実行
this.scrollY = this.sn.scrollY; //画面上部からのスクロール量
this.targetPos = this.target.getBoundingClientRect().top + this.scrollY; //targetのページ上部からの位置
this.targetScroll =
this.scrollY - this.targetPos + innerHeight + this.rootMargin; ///任意の要素がrootMarginの位置に来たときにスクロール量を0としてscrollMaxまで変化する
this.resultNormal = 0; //linearでmin~maxの間を変化する変数
this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
this._getResultNum();//スクロール量を取得して値を更新する
}
//正規化
_norm(v, a, b) {
return (v - a) / (b - a);
}
//線形補完
_lerp(a, b, t) {
return a + (b - a) * t;
}
_getResultNum() {
//要素が画面内に最初からいる時(FV内にある時)スクロール量をそのまま正規化に使用する
if (this.targetPos < innerHeight + this.target.clientHeight) {
window.addEventListener('scroll', function () {
this.sn.getResult();
this.scrollY = this.sn.scrollY;
if (this.scrollY > this.scrollMax) {
this.scrollY = this.scrollMax;
}
const normalizeNum = this._norm(this.scrollY, 0, this.scrollMax); //0~1
const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
}.bind(this));
} else {
window.addEventListener(
"scroll",
function () {
this.sn.getResult();
this.scrollY = this.sn.scrollY;
this.targetScroll =
this.scrollY - this.targetPos + innerHeight + this.rootMargin;
//要素が画面内に入ってからのスクロール量を0~this.scrollMaxに留める
if (this.targetScroll !== undefined && this.targetScroll < 0) {
this.targetScroll = 0;
} else if (
this.targetScroll !== undefined &&
this.targetScroll > this.scrollMax
) {
this.targetScroll = this.scrollMax;
}
const normalizeNum = this._norm(this.targetScroll, 0, this.scrollMax); //0~1
const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
}.bind(this)
);
}
}
}
//使い方
//target(第一引数)にはElementを入れる = document.getElementByID('任意の要素');
//イベント外でインスタンス化
//スクロールイベント内で欲しい値を取得(resultEase or resultNormal)
//targetの変更したいスタイルに取得した数値を適応する
//--------------------------------------------------------
//ここから活用例
//イージング関数を変数に格納
const ease = function easeInOutBack(x) {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return x < 0.5
? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
: (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
};
const complete = new ScrollFunction(
document.getElementById("target"),
500,
-100,
ease,
300,
1000
);
window.addEventListener('scroll', function () {
const transX = complete.resultEase;
complete.target.style.transform = `translateX(${transX}px)`;
}, { passive: true });
上記のコードでは、targetが画面端から100px(rootMargin)の位置にきたらアニメーションを開始。
500px(scrollMax)スクロールする間に、300(min)から1000(max)に変化する変数を定義(transX)
それをスクロールイベント内でtargetのスタイルに適応することで、
transform:translateX(300px) ←→ transform:translateX(1000px)
の間で変化させることができます。
以下,codepenで挙動をご確認ください。(PC推奨)
使っているイージング関数はこちらを参照しています。
codepenに3つだけイージング関数を用意しているので
変更して動きがどうなるか試してみてください。
最後に
自分で作ったクラスながら、いざ言語化して説明するとなるとなかなか難しかったです。
まだまだ不備やパフォーマンスへの懸念点も多々あるので、改善できたらまた共有させて頂きます。
Discussion