رابطهای سینمایی در Next.js؛ وقتی انیمیشن کار میکند، نه فقط تکان میخورد
انیمیشن خوب دیده نمیشود، حس میشود. تجربهی عملی ساختن رابطهای متحرک با Next.js، Motion و Anime.js — و مهمتر از آن، جایی که باید جلوی خودت را بگیری.
Mehran Hatami4 دقیقه مطالعهNext.jsAnime.jsMotion Designاولین انیمیشنی که برای یک پنل واقعی نوشتم خیلی چشمگیر بود — و دقیقاً به همین دلیل بد بود. کارتها میچرخیدند، عددها بالا میپریدند، همهچیز برق میزد. سه هفته بعد همان مشتری گفت پنل «سنگین» شده. هیچچیز کند نشده بود؛ فقط هر کلیک یک نمایش کوچک راه میانداخت که کسی سفارشش را نداده بود.
انیمیشن یعنی جواب دادن، نه تزئین کردن
یک انیمیشن وقتی درست است که اگر حذفش کنی، کاربر گم شود؛ نه اینکه صفحه فقط ساکتتر به نظر برسد.
حرکت در رابط کاربری یک کار مشخص دارد: به کاربر بگوید چه اتفاقی افتاد و چیزی که دنبالش بود الان کجاست. یک مودال که از دکمهی خودش باز میشود، رابطهی علت و معلولی را نشان میدهد. همان مودال که وسط صفحه fade میشود، فقط یک تأخیر ۳۰۰ میلیثانیهای است. تفاوت این دو در زیبایی نیست، در معنا است.
معیار عملی من ساده شده: انیمیشن را حذف کن. اگر کاربر گیج میشود، انیمیشن لازم بوده. اگر فقط صفحه ساکتتر شده، آن انیمیشن هزینه بوده نه سرمایه.
چرا هم Motion و هم Anime.js
این دو رقیب هم نیستند؛ دو جنس متفاوت از حرکتاند و در یک پروژه کنار هم مینشینند.
- Motion (Framer Motion) برای حرکتی که به تعامل گره خورده است: باز و بسته شدن، جابهجایی بین حالتها، layout و shared element. چون فیزیکمحور است، وسط کار میشود قطعش کرد و مسیر همچنان طبیعی میماند.
- Anime.js برای حرکتی که تایملاین دارد: ورود مرحلهای یک لیست، شمارش عدد، دنبالهای از قدمها با فاصلههای دقیق. تابع stagger کاری را در دو خط انجام میدهد که با useEffect و setTimeout سی خط میشود.
- قاعدهی من: هرچه کاربر مستقیم لمسش میکند با Motion، هرچه خودش را روایت میکند با Anime.js.
منحنی مهمتر از مدتزمان است
بیشتر انیمیشنهای بد، مشکل زمانبندی ندارند؛ مشکل منحنی دارند. یک ease خطی، حرکت را مکانیکی میکند. یک فنر پرجهش، به سطحی که فقط «رسیده» انرژیای نسبت میدهد که هرگز نداشته است. من پیشفرض را روی فنر میرای بحرانی میگذارم و جهش را فقط جایی خرج میکنم که کاربر واقعاً چیزی را پرتاب کرده باشد.
// پیشفرض: میرسد، پرتاب نمیشود
const CALM = { type: "spring", bounce: 0, duration: 0.5 } as const;
// استثنا: یک تأیید که باید مثل مُهر بنشیند
const STAMP = { type: "spring", bounce: 0.35, duration: 0.4 } as const;
<motion.div layout transition={CALM} />سه قانونی که دیگر از آنها عبور نمیکنم
- فقط transform و opacity متحرک شوند. هر چیز دیگری مرورگر را مجبور به layout دوباره میکند و روی موبایل ارزان، مستقیم به لگ ختم میشود.
- هیچ انیمیشنی مسیر کاربر را قفل نکند. اگر کاربر وسط انیمیشن کلیک کرد، باید کارش انجام شود، نه اینکه منتظر پایان نمایش بماند.
- حرکت تکرارشونده باید زیر آستانهی توجه بماند. یک درخشش آرام هفتثانیهای پسزمینه است؛ همان درخشش در یک ثانیه، مزاحم است.
و تنظیمی که هیچوقت فراموشش نمیکنم
بخشی از کاربران با حرکت زیاد دچار سرگیجه میشوند و این را در تنظیمات سیستمعاملشان اعلام کردهاند. احترام گذاشتن به آن یک خط کد است و تفاوتش برای آن کاربر، تفاوت بین قابلاستفاده و غیرقابلاستفاده بودن محصول است.
const reduced = useReducedMotion();
<motion.div
initial={reduced ? undefined : { opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }}
transition={reduced ? { duration: 0 } : CALM}
/>رابط سینمایی یعنی رابطی که ریتم دارد، نه رابطی که شلوغ است. وقتی درست کار کند، کاربر دربارهی انیمیشن حرف نمیزند — میگوید کار کردن با این پنل «راحت» است. همان جمله، بهترین بازخوردی است که یک انیمیشن میتواند بگیرد.
