🎢
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