🎢

VanillaJSでカスタムイージング対応のスムーススクロール

に公開

CSSアニメーションと同様に、VanillaJSでもcubic-bezierでイージングカーブをコントロールできるようにしたのでコードを共有します。使う場面が多いページ内のスムーススクロールとして一式まとめてますが、部分的にも使えると思いますので😋 良かったらご活用ください!
(ライブラリ不要、プレーンJS、GSAPやanime.js とも共存OK)

ポイント

1. CSS Easing Parser

- cubic-bezier() のパース & ベジェ曲線計算(ニュートン法)
- linear() のパース & 線形補間計算

2. カスタムイージング対応スクロール

- CSS記法がそのまま使える!!(cubic-bezier(), linear())
- CSS標準キーワード対応(ease, ease-in, ease-out, ease-in-out, linear)
- 引数はtargetY, duration, easingの3つ
- 引数バリデーション完備(console.error() で実装ミスを検知)
- デフォルト引数で簡易使用も可(targetY = 0, duration = 777, easing = 'ease-in-out')

3. シンプル版スクロール(ブラウザ標準)

- behavior: 'smooth'( または'auto': 瞬時に移動の2択)
- durationは ブラウザが自動計算(距離に応じて可変)
- デフォルト引数で簡易使用も可(targetY = 0:ページトップへ戻る)

4. スクロールタイプとタイム、イージングの設定

- スクロール実行タイプ(カスタム版/シンプル版)をチョイス
- アニメーション時間とイージングカーブを柔軟に調整可能
- 各パターンと簡易的な使用例付き

コード

/* 
スムースにスクロール 
----------------------------------------------------------------*/

// カスタムイージング対応版
// # CSS記法がそのまま使える!!(cubic-bezier(), linear(), & ease, ease-in, ease-out, ease-in-out, linear)
function smoothScrollTo(targetY = 0, duration = 777, easing = 'ease-in-out') {
	// 引数バリデーション
	if (typeof targetY !== 'number' || isNaN(targetY)) {
		console.error('[smoothScrollTo] Invalid targetY:', targetY);
		return;
	}
	if (typeof duration !== 'number' || isNaN(duration) || duration <= 0) {
		console.error('[smoothScrollTo] Invalid duration:', duration, '- Using default 777ms');
		duration = 777;
	}
	if (typeof easing !== 'string') {
		console.error('[smoothScrollTo] Invalid easing:', easing, '- Using default "ease-in-out"');
		easing = 'ease-in-out';
	}
	
	const startY = window.pageYOffset;
	const distance = targetY - startY;
	const startTime = performance.now();
	
	// イージング関数を決定
	let easingFunc;
	
	if (easing === 'linear') {
		easingFunc = t => t;
	} else if (easing.startsWith('cubic-bezier')) {
		const params = EasingParser.parseCubicBezier(easing);
		if (params && params.length === 4) {
			easingFunc = t => EasingParser.cubicBezier(t, ...params);
		} else {
			console.error('[smoothScrollTo] Invalid cubic-bezier format:', easing, '- Fallback to linear');
			easingFunc = t => t;
		}
	} else if (easing.startsWith('linear(')) {
		const values = EasingParser.parseLinear(easing);
		if (values && values.length > 0) {
			easingFunc = t => EasingParser.linear(t, values);
		} else {
			console.error('[smoothScrollTo] Invalid linear() format:', easing, '- Fallback to linear');
			easingFunc = t => t;
		}
	// CSS標準キーワード値:実用抜粋
	} else if (easing === 'ease') {
		easingFunc = t => EasingParser.cubicBezier(t, 0.25, 0.1, 0.25, 1);
	} else if (easing === 'ease-in') {
		easingFunc = t => EasingParser.cubicBezier(t, 0.42, 0, 1, 1);
	} else if (easing === 'ease-out') {
		easingFunc = t => EasingParser.cubicBezier(t, 0, 0, 0.58, 1);
	} else if (easing === 'ease-in-out') {
		easingFunc = t => EasingParser.cubicBezier(t, 0.42, 0, 0.58, 1);
	} else {
		console.error('[smoothScrollTo] Unknown easing:', easing, '- Fallback to linear');
		easingFunc = t => t;
	}
	
	// requestAnimationFrame でアニメーション
	function step(currentTime) {
		const elapsed = currentTime - startTime;
		const progress = Math.min(elapsed / duration, 1);
		const eased = easingFunc(progress);
		
		window.scrollTo(0, startY + distance * eased);
		
		if (progress < 1) {
			requestAnimationFrame(step);
		}
	}
	
	requestAnimationFrame(step);
}

// 計算機:CSS Easing Parser & Calculator
const EasingParser = {
	// cubic-bezier() のパース
	parseCubicBezier(str) {
		const match = str.match(/cubic-bezier\(([\d\.,\s]+)\)/);
		if (!match) return null;
		return match[1].split(',').map(v => parseFloat(v.trim()));
	},
	
	// linear() のパース
	parseLinear(str) {
		const match = str.match(/linear\(([\d\.,\s]+)\)/);
		if (!match) return null;
		return match[1].split(',').map(v => parseFloat(v.trim()));
	},
	
	// cubic-bezier 計算(ベジェ曲線)
	cubicBezier(t, p1x, p1y, p2x, p2y) {
		// ニュートン法でtに対応するxを求める
		const cx = 3.0 * p1x;
		const bx = 3.0 * (p2x - p1x) - cx;
		const ax = 1.0 - cx - bx;
		
		const cy = 3.0 * p1y;
		const by = 3.0 * (p2y - p1y) - cy;
		const ay = 1.0 - cy - by;
		
		// t に対する x を計算
		function sampleCurveX(t) {
			return ((ax * t + bx) * t + cx) * t;
		}
		
		// t に対する y を計算
		function sampleCurveY(t) {
			return ((ay * t + by) * t + cy) * t;
		}
		
		// x から t を逆算(ニュートン法)
		function solveCurveX(x) {
			let t2 = x;
			for (let i = 0; i < 8; i++) {
				const x2 = sampleCurveX(t2) - x;
				if (Math.abs(x2) < 0.000001) return t2;
				const d2 = (3.0 * ax * t2 + 2.0 * bx) * t2 + cx;
				if (Math.abs(d2) < 0.000001) break;
				t2 = t2 - x2 / d2;
			}
			return t2;
		}
		
		return sampleCurveY(solveCurveX(t));
	},
	
	// linear() 計算(補間)
	linear(t, values) {
		if (values.length === 0) return t;
		if (values.length === 1) return values[0];
		
		// NaN チェック
		if (values.some(v => isNaN(v))) {
			console.error('[EasingParser.linear] Invalid values contain NaN:', values);
			return t; // linear にフォールバック
		}
		
		const segments = values.length - 1;
		const scaledT = t * segments;
		const segment = Math.floor(scaledT);
		const segmentT = scaledT - segment;
		
		const startValue = values[Math.min(segment, segments)];
		const endValue = values[Math.min(segment + 1, segments)];
		
		return startValue + (endValue - startValue) * segmentT;
	}
};

// シンプル版(ブラウザ標準)
// # durationは ブラウザが自動計算(距離に応じて可変)
function simpleScrollTo(targetY = 0) {
	window.scrollTo({
		top: targetY,
		behavior: 'smooth'	// or 'auto': 瞬時に移動
	});
}

// スムーススクロール実装
function scrollGroovy() {
	document.querySelectorAll('a[href^="#"]').forEach(anchor => {
		anchor.addEventListener('click', (e) => {
			const href = anchor.getAttribute('href');
			const target = (href === '#' || href === '') ? document.documentElement : document.querySelector(href);
			const hashArray = ['#---', '#hoge'];	// ← 特定のIDを除外する場合はこちらに
			
			if (!hashArray.includes(href)) {
				e.preventDefault();
				const position = target.offsetTop;
				
				// シンプル版(ブラウザ標準)
				// simpleScrollTo(position);

				// カスタムイージング対応版
				// smoothScrollTo(position, 333, 'linear');
				smoothScrollTo(position, 777, 'cubic-bezier(0.75,0,0,1)');
				// smoothScrollTo(position, 1000, 'linear(0, 0.0874, 0.2047, 0.3429, 0.4929, 0.6464, 0.7961, 0.9357, 1.06, 1.1656, 1.25, 1.3122, 1.3521, 1.371, 1.3706, 1.3536, 1.3227, 1.2812, 1.2323, 1.1793, 1.125, 1.0721, 1.0227, 0.9788, 0.9415, 0.9116, 0.8896, 0.8755, 0.8688, 0.869, 0.875, 0.8859, 0.9006, 0.9179, 0.9366, 0.9558, 0.9745, 0.992, 1.0075, 1.0207, 1.0313, 1.039, 1.044, 1.0464, 1.0463, 1.0442, 1.0403, 1.0351, 1.029, 1.0224, 1.0156, 1.009, 1.0028, 0.9973, 0.9927, 0.989, 0.9862, 0.9844, 0.9836, 0.9836, 0.9844, 0.9857, 0.9876, 0.9897, 0.9921, 0.9945, 0.9968, 0.999, 1.0009, 1.0026, 1.0039, 1.0049, 1.0055, 1.0058, 1.0058, 1.0055, 1.005, 1.0044, 1.0036, 1.0028, 1.002, 1.0011, 1.0004, 0.9997, 0.9991, 0.9986, 0.9983, 0.9981, 0.998, 0.998, 0.998, 0.9982, 0.9984, 0.9987, 0.999, 0.9993, 0.9996, 0.9999, 1.0001, 1.0003, 1)');
				
				// 【簡易使用】ページトップへ(タイムとイージングは初期値:777ms, ease-in-out)
				// smoothScrollTo();

				// *引数不足でも動作*
				// smoothScrollTo(position);	// 位置指定のみ
				// smoothScrollTo(500);			// 位置指定(固定)のみ
				// smoothScrollTo(0, 1000);		// 位置とタイム指定のみ

			}
		});
	});
}
scrollGroovy();

Discussion