نحوه اسکرول نرم در صفحه ( Smooth Scrolling )
در این آموزش قصد داریم به شکل ساده و بدون نیاز به کتابخانه ای خاص، اسکرول صفحه وب خود را تغییر داده و افکت اسکرول نرم را به آن اضافه کنیم
تفکر کلی
قصد ما این است که یک در بر گیرنده کلی با position
ثابت ( fixed
) و همچنین overflow:hidden
و یک فرزند مستقیم که بشه حرکتش داد ( وقتی اسکرول می کنیم ). ارتفاع تگ body
رو به اندازه همین فرزند میکنیم تا اسکرول صفحه به نمایش در بیاد. وقتی صفحه اسکرول میشه، آیتم که در ابتدای این توضیح موقعیتش رو در صفحه ثابت کردیم، جای خودش باقی میمونه ولی ما موقعیت فرزندش رو تغییر میدیم. این تکنیک باعث بوجود آمدن یک اسکرول نرم در برای صفحه شما می شود.
ساختار HTML
ساختار کلی صفحه در این مثال به شکل زیر است:
<body class="loading">
<main>
<div data-scroll>
<!-- ... --->
</div>
</main>
</body>
تگ main
در صفحه ثابت خواهد شد و تگی که دارای ویژگی [data-scroll]
می باشد، در صفحه translate
خواهد شد.
JAVASCRIPT
حالا نیاز به کدهای جاوا اسکریپت داریم. اول از همه چند متد کمکی و متغیرهایی برای کار خودمون درست کنیم
const MathUtils = {
// map number x from range [a, b] to [c, d]
map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
// linear interpolation
lerp: (a, b, n) => (1 - n) * a + n * b
};
const body = document.body;
برای محاسبات بعدی به سایز پنجره بخصوص ارتفاع آن نیاز داریم.
let winsize;
const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
calcWinsize();
همچنین نیاز داریم این مقادیر را در هنگام تغییر سایز صفحه از نو محاسبه کنیم
window.addEventListener('resize', calcWinsize);
و همچنین نیاز داریم میزان اسکرول انجام شده را دنبال کنیم
let docScroll;
const getPageYScroll = () => docScroll = window.pageYOffset || document.documentElement.scrollTop;
window.addEventListener('scroll', getPageYScroll);
حالا که توابع کمکی مورد نیاز را داریم، بیایید بدنه اصلی کد را بنویسیم. ابتدا یک کلاس برای اسکرول نرم ایجاد می کنیم
class SmoothScroll {
constructor() {
this.DOM = {main: document.querySelector('main')};
this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');
...
}
}
new SmoothScroll();
تا اینجا ما یک عنصر اصلی که همان main
هست داریم ( همان المان دربرگیرنده ای که باید استایل fixed
داشته باشد ) و فرزندی که باید اسکرول شود ( فرزندی که قرار است با translate
اسکرول کردن را با آن شبیه سازی کنیم ).
حالا میخواهیم translateY
را در هنگام اسکرول آپدیت کنیم اما ممکن است سایر ویژگی ها مانند scale
و یا rotation
را نیز بخواهیم آپدیت کنیم. پس یک شیئ ( object ) ایجاد می کنیم تا این تنظیمات را نگه دارد. در حال حاضر، فقط translationY
constructor() {
...
this.renderedStyles = {
translationY: {
previous: 0,
current: 0,
ease: 0.1,
setValue: () => docScroll
}
};
}
در ادامه برای بدست آوردن یک اسکرول نرم، از میزان اسکرول قبل و میزان اسکرولی که الان درش هستیم استفاده میکنیم. متغیرهای "previous
" و "current
" برای این کار در نظر گرفته شده اند. مقدار translationY
فعلی، مقداری بین این دو متغیر است که کم کم افزایش می یابد. مقدار ease
سرعت این تغییر بین دو متغیر را کنترل می کند. فرمول زیر برای محاسبه مقدار translation
فعلی استفاده می شود.
previous = MathUtils.lerp(previous, current, ease)
تابع setValue
مقدار فعلی را ست می کند که در این مورد، مقدار فعلی اسکرول می باشد. پس این تنظیمات را در صفحه در هنگام لود شدن صفحه انجام می دهیم تا موقعیت درست translationY
بدست آید.
constructor() {
...
this.update();
}
update() {
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
}
this.layout();
}
layout() {
this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;
}
هر دو متغیر را ( current
و previous
) را در ابتدا یکسان در نظر گرفتیم. و فقط در هنگام اسکرول صفحه این مقادیر باید تغییر کنند تا اسکرول صفحه با انیمیشن همراه شود. برای این کار تابع layout
صدا زده می شود تا transformation
را بر روی المان مورد نظر ما ست کند. دقت کنید مقدار آن در زمان اسکرول به سمت بالا منفی خواهد بود.
برای تغییرات لایه، به موارد زیر نیاز داریم:
- تگ
main
را فیکس و مقدار اورفلو آن را مخفی (overflow:hidden
) کنیم تا به بالای صفحه بچسبد و اسکرول نشود. - ارتفاع تگ
body
را به اندازه ارتفاع مقدار محتوایی که داریم خواهیم کرد تا اسکرول صفحه نمایش داده شود.
constructor() {
...
this.setSize();
this.style();
}
setSize() {
body.style.height = this.DOM.scrollable.scrollHeight + 'px';
}
style() {
this.DOM.main.style.position = 'fixed';
this.DOM.main.style.width = this.DOM.main.style.height = '100%';
this.DOM.main.style.top = this.DOM.main.style.left = 0;
this.DOM.main.style.overflow = 'hidden';
}
همچنین نیاز داریم ارتفاع body
را در هنگامی که سایز صفحه تغییر می کند از نو ست کنیم
constructor() {
...
this.initEvents();
}
initEvents() {
window.addEventListener('resize', () => this.setSize());
}
حالا تابع خود را می زنیم تا مقادیر را بر طبق اسکرول انجام شده آپدیت کند
constructor() {
...
requestAnimationFrame(() => this.render());
}
render() {
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
}
this.layout();
// for every item
for (const item of this.items) {
// if the item is inside the viewport call it's render function
// this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
if ( item.isVisible ) {
item.render();
}
}
// loop..
requestAnimationFrame(() => this.render());
}
در ادامه کلاس loading را از تگ body حذف و موقعیت scroll را می گیریم ( اگر قبل از آخرین رفرش صفحه را اسکرول کرده باشیم، این مقادیر ممکن است صفر نباشد ) و SmoothScroll را مقدار دهی اولیه می کنیم.
document.body.classList.remove('loading');
// Get the scroll position
getPageYScroll();
// Initialize the Smooth Scrolling
new SmoothScroll(document.querySelector('main'));
و حالا اسکرول نرم ایجاد شده.