اسکرول بیپایان با GSAP و Lenis
خلاصهٔ کاملتر
نویسنده میگه این افکت اولش برای یه پروژهٔ مشتری ساخته شده بود با یه بریف ساده: سبک، سریع و در حد MVP. نکتهش اینه که ساده بهراحتی تبدیل به خالی میشه، پس بهجای اضافهکردن چیزهای بیشتر، جواب رو تو حرکت پیدا کرده. حلقهٔ یکپارچه توقف سختِ ته صفحه رو حذف میکنه، پارالاکس بین سکشنها عمق میسازه و کنترل اسنپ به تجربه ریتم میده. نسخهٔ اصلی با Next.js و TypeScript بود، ولی تو آموزش همهچی به HTML و CSS و JS ساده تقلیل پیدا کرده.
معماریش قبل از هر خط انیمیشن روشن میشه: چند سکشن تمامصفحه بساز، سکشن اول رو در انتها تکرار کن، حالت infinite رو در Lenis روشن کن، روی هر سکشن اسنپ بذار و مدیای داخلش رو با ScrollTrigger انیمیت کن. اون سکشن تکراری همون چیزیه که ریستشدن صفحه رو نامرئی میکنه؛ بدون اون، کاربر پرش رو حس میکنه. برای اینکه اسکرینریدر دو بار نخوندش هم با aria-hidden از دید فناوری کمکی پنهان میشه.
سمت CSS، به گفتهٔ نویسنده سه سکشن نقطهٔ شیرینه: دوتا خالی حس میشه، سهتا به بالا عمدی. هر سکشن کل ویوپورت رو پر میکنه و بهجای 100vh از 100svh استفاده میشه تا نوار ابزار موبایل سافاری دردسر درست نکنه. ساختار هم سادهست: سکشن تمامصفحه، کانتینر مدیا با position: absolute و inset: 0 که کل سکشن رو پر میکنه، و تصویری با object-fit: cover.
منطق جاوااسکریپت دو تیکهست. تیکهٔ اول سیستم اسکروله، جایی که خود حلقه اتفاق میافته:
const lenis = new Lenis({ infinite: true });
const snap = new Snap(lenis, {
type: 'mandatory',
duration: 0.9,
});
snap.addElements(document.querySelectorAll('section'), { align: 'start' });نویسنده تأکید میکنه بیشتر جادو تو همون یک خط infinite: true جمع شده و بقیه فقط کنترل و پرداخته. Lenis هم با رویداد اسکرولش ScrollTrigger.update رو صدا میزنه و از تیکر GSAP تغذیه میشه تا هر دو روی یه حلقهٔ فریم بشینن.
تیکهٔ دوم پارالاکسه؛ جایی که به قول نویسنده کار از «درست کار میکنه» به «حس خوبی داره» میرسه. مدیای هر سکشن از منفی ۵۰ درصد به مثبت ۵۰ درصد در محور عمودی حرکت میکنه و با scrub به اسکرول بسته میشه:
gsap.fromTo(media,
{ yPercent: -50 },
{ yPercent: 50, ease: 'none',
scrollTrigger: { trigger: hero, start: 'top bottom', end: 'bottom top', scrub: true }
}
);همین تأخیر کوچک بین اسکرول و حرکت مدیاست که لایهها رو از هم جدا میکنه و صفحه رو از حالت تخت درمیاره.
آخرین مرحله اصلاح iOSه. با باز و بستهشدن نوار ابزار سافاری، ارتفاع ویوپورت عوض میشه و درز حلقه لو میره. راهحل، تعریف کانتینر اسکرول اختصاصیه: یه wrapper با ارتفاع 100svh و overflow: hidden که به Lenis معرفی میشه، و بعد ScrollTrigger.scrollerProxy تا GSAP هم موقعیت اسکرول رو از همون کانتینر بخونه بهجای پنجره. با تنظیم scroller روی همون wrapper در انیمیشنها، حلقه بدون پرش و لرزش کار میکنه.
نکات کلیدی:
- تکرار سکشن اول در انتها، چیزیه که درز حلقه رو نامرئی میکنه
- infinite: true در Lenis بیشتر کار حلقه رو انجام میده
- 100svh بهجای 100vh برای پرهیز از دردسر نوار ابزار موبایل
- پارالاکس با حرکت منفی ۵۰ تا مثبت ۵۰ درصد و scrub ساخته میشه
- برای iOS باید کانتینر اسکرول اختصاصی و scrollerProxy تعریف کنی




