توسعه فرانت‌اند

Web Animations API: جایگزین jQuery animate

کنترل Animation و Promise پایان، تفاوت لغو و موفقیت عمل، کاهش حرکت و مهاجرت از jQuery؛ نمونه مستقل با pause و cancel.

1405/01/03 3 دقیقه مطالعه 896 بازدید

یک پنل باز می‌شود و کاربر پیش از پایان حرکت، آن را می‌بندد. اگر انیمیشن فقط با یک زنجیره از تغییرهای style اجرا شده باشد، ممکن است حرکت قبلی همچنان ادامه پیدا کند. Web Animations API کنترل‌هایی برای پخش و لغو فراهم می‌کند تا این رابطه روشن‌تر شود. با این حال، مهاجرت از jQuery animate یک جایگزینی لفظی نیست؛ رفتار صف، propertyها و وضعیت نهایی باید جدا بررسی شوند.

برای انتخاب روش، ابتدا نقش حرکت را بنویسید. آیا توجه را به تغییر محتوا جلب می‌کند، ارتباط دو حالت را نشان می‌دهد یا فقط تزئینی است؟ در بسیاری از حالت‌های ساده، CSS transition کافی است. وقتی کنترل برنامه‌ای، توقف یا هماهنگی زمان لازم است، WAAPI می‌تواند مناسب باشد. انتخاب ابزار نباید باعث شود منطق اصلی محصول به پایان یک حرکت نمایشی وابسته شود.

Animation و Element نقش متفاوت دارند

مرجع Web Animations API مدل حرکت و زمان‌بندی را توضیح می‌دهد. element.animate یک Animation می‌سازد و آن را اجرا می‌کند. object بازگشتی برای کنترل پخش نگه داشته می‌شود. اگر reference را دور بیندازید، مدیریت همان حرکت مشخص دشوارتر می‌شود. داشتن API مشترک مرورگر به معنی کنترل خودکار همه stateهای رابط نیست و باید عمر animation را در برنامه تعریف کنید.

راهنمای animate درباره keyframeها و گزینه‌ها توضیح دارد. مقدارها باید propertyهای قابل انیمیشن و قرارداد همان عنصر را رعایت کنند. مدت، easing و تکرار را با هدف انتخاب کنید. زمان ثابت همه حرکت‌های محصول را بهتر نمی‌کند. در صفحه کوچک، مسافت واقعی می‌تواند متفاوت باشد و حرکت باید با فضای موجود هماهنگ بماند.

نمونه کامل حرکت محدود

فایل زیر را به نام animation.html و UTF-8 ذخیره و به صورت HTML باز کنید. CSS و JavaScript داخل همان فایل‌اند؛ تصویر، بسته یا API خارجی ندارند. یک مربع تزئینی داخل track قرار دارد و با انتخاب شروع، به اندازه فضای موجود به چپ می‌رود و بازمی‌گردد. مدت یک چرخه ۱۸۰۰ میلی‌ثانیه و تکرار یک‌بار است. مقدار، انتخاب آموزشی است و نشان‌دهنده سرعت استاندارد همه رابط‌ها نیست.

<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>کنترل حرکت نمونه</title><style>*{box-sizing:border-box}body{font-family:Tahoma,Arial,sans-serif;line-height:1.9;background:#f2f4f7;color:#17243b;margin:0}main{width:min(38rem,calc(100% - 2rem));margin:auto;padding-block:2rem}.track{display:flex;align-items:center;height:4rem;border:1px solid #8c9db5;background:white;overflow:hidden}.dot{width:2rem;height:2rem;background:#173f78;flex:none}button{font:inherit;padding:.7rem;margin:.5rem;border:1px solid #66768d;background:white;color:#17243b;border-radius:.4rem}:focus-visible{outline:3px solid #946000;outline-offset:3px}</style></head><body><main><h1>حرکت آموزشی رفت‌وبرگشت</h1><div class="track" id="track" aria-hidden="true"><div class="dot" id="dot"></div></div><button id="start" type="button">شروع یا ادامه</button><button id="pause" type="button">توقف موقت</button><button id="cancel" type="button">لغو حرکت</button><p id="status" role="status">حرکت هنوز شروع نشده است.</p></main><script>
const dot=document.getElementById('dot');const track=document.getElementById('track');const status=document.getElementById('status');const reduced=matchMedia('(prefers-reduced-motion: reduce)');let current=null;
function cancel(message){const old=current;current=null;if(old)old.cancel();status.textContent=message;}
document.getElementById('start').addEventListener('click',()=>{if(reduced.matches){cancel('ترجیح کاهش حرکت فعال است؛ حرکت تزئینی اجرا نمی‌شود.');return;}if(typeof dot.animate!=='function'){status.textContent='این محیط API لازم را ارائه نمی‌کند.';return;}if(current&&current.playState==='paused'){current.play();status.textContent='حرکت ادامه یافت.';return;}cancel('در حال شروع چرخه تازه.');const distance=Math.max(0,track.clientWidth-dot.clientWidth);const animation=dot.animate([{transform:'translateX(0)'},{transform:`translateX(${-distance}px)`},{transform:'translateX(0)'}],{duration:1800,iterations:1,easing:'ease-in-out'});current=animation;status.textContent='حرکت آغاز شد.';animation.finished.then(()=>{if(current===animation){current=null;status.textContent='حرکت تمام شد.';}}).catch(()=>{if(current===animation){current=null;status.textContent='حرکت پایان طبیعی نداشت.';}});});
document.getElementById('pause').addEventListener('click',()=>{if(current&&current.playState==='running'){current.pause();status.textContent='حرکت موقتاً متوقف شد.';}});document.getElementById('cancel').addEventListener('click',()=>cancel('حرکت لغو شد.'));reduced.addEventListener('change',()=>{if(reduced.matches)cancel('ترجیح کاهش حرکت فعال شد.');});
</script></body></html>

دکمه شروع در حالت pause همان حرکت را ادامه می‌دهد؛ در حالت‌های دیگر یک چرخه تازه می‌سازد. توقف موقت فقط وقتی حرکت جاری وجود دارد اثر می‌کند. لغو reference را کنار می‌گذارد و مربع به style پایه برمی‌گردد. پایان طبیعی و لغو مسیرهای پیام جدا دارند. هنگام شروع دوباره، نتیجه Promise حرکت قبلی اجازه ندارد خبر چرخه تازه را بی‌دلیل عوض کند.

لغو با پایان متفاوت است

مرجع cancel حذف اثر حرکت و وضعیت مربوط را شرح می‌دهد. مرجع finished نیز Promise پایان را توضیح می‌دهد. لغو می‌تواند Promise جاری را reject کند و باید مدیریت شود. خطای ناشی از لغو مورد انتظار با شکست منطق کسب‌وکار یکی نیست. در نمونه، catch وجود دارد و نتیجه به همان object جاری مربوط می‌شود.

پایان انیمیشن را معادل موفقیت ذخیره یا دریافت داده ندانید. اگر حرکت بعد از ارسال درخواست نمایش داده می‌شود، نتیجه معتبر از مسیر عملیات می‌آید. زمان‌بندی بصری می‌تواند با آن هماهنگ شود، ولی نباید خبر موفقیت زودهنگام بسازد. راهنمای Microinteractions درباره رابطه عمل، state و بازخورد توضیح دارد. حرکت فقط یک نوع بازخورد است.

Fill و style نهایی

گاهی طراح می‌خواهد عنصر پس از پایان در موقعیت تازه بماند. effect و style پایه دو لایه متفاوت‌اند و باید تصمیم بگیرید کدام منبع وضعیت نهایی است. نگه داشتن effect با fill می‌تواند ظاهر را حفظ کند، اما state برنامه را خودبه‌خود تغییر نمی‌دهد. اگر چند حرکت روی یک property اثر می‌گذارند، نتیجه باید با قرارداد ترکیب و اولویت شناخته شود، نه با فرض اینکه آخرین کلیک همیشه همه چیز را پاک می‌کند.

در نمونه، مسیر رفت‌وبرگشت به نقطه پایه تمام می‌شود و fill خاصی نگه داشته نشده است. بنابراین لازم نیست برای همین مثال transform دائمی ثبت شود. این محدودیت باعث می‌شود لغو و پایان قابل فهم باشند. برای پنل یا جابه‌جایی واقعی محتوا، style و DOM باید وضعیت نهایی را نمایندگی کنند. استفاده از animation برای پنهان کردن فاصله میان state و ظاهر می‌تواند نتیجه ناپایدار بسازد.

حرکت و ترجیح کاربر

مرجع prefers-reduced-motion راه شناخت ترجیح کاهش حرکت را توضیح می‌دهد. نمونه قبل از شروع آن را بررسی می‌کند و در حالت کاهش، حرکت تزئینی را اجرا نمی‌کند. اگر ترجیح در حین پخش تغییر کند، حرکت لغو می‌شود. اطلاعات و کنترل‌ها باقی می‌مانند؛ حذف انیمیشن نباید کار اصلی را ناممکن کند.

برای حرکت دارای معنی، می‌توان مسیر کم‌حرکت‌تری ارائه کرد که همان تغییر وضعیت را توضیح دهد. فقط کوتاه کردن زمان یک جابه‌جایی بزرگ همیشه پاسخ مناسبی نیست. flashing و حرکت تکراری نیز نیاز به تصمیم جدا دارند. مخاطب باید امکان فهم نتیجه بدون دنبال کردن جسم متحرک داشته باشد. کنترل pause برای یک حرکت طولانی می‌تواند لازم باشد، اما به‌تنهایی همه نیازهای دسترس‌پذیری را حل نمی‌کند.

عملکرد تضمین GPU نیست

WAAPI می‌تواند از سازوکار مرورگر استفاده کند، ولی عبارت «همه انیمیشن‌ها GPU-accelerated هستند» دقیق نیست. property، اندازه محتوا و شرایط اجرا اثر دارند. حرکت transform یا opacity اغلب با تغییر عرض و جریان layout مسئله متفاوتی دارد. اگر اندازه‌گیری و نوشتن style در هر مرحله اضافه می‌کنید، همان منطق JavaScript هم هزینه دارد. نام API جای بررسی دامنه واقعی کار را نمی‌گیرد.

در نمونه، مسافت هنگام شروع اندازه‌گیری می‌شود و loop دستی برای هر frame وجود ندارد. این انتخاب برای حرکت ساده مناسب است. اگر اندازه ظرف در حین پخش تغییر کند، مسیر قبلی خودکار دوباره محاسبه نشده است؛ برای محصول واقعی باید سیاست این حالت را تعیین کنید. راهنمای اندازه میزبان درباره رابطه فضا و مؤلفه توضیح دارد، ولی query CSS کنترل زمان‌بندی animation نیست.

مهاجرت از کد قدیمی

فهرست propertyها، مدت‌ها، callbackها و رفتار stop در کد موجود را بخوانید. یک queue قدیمی ممکن است چند مرحله را پشت هم اجرا کند و نمونه animate تازه به‌طور پیش‌فرض همان قرارداد را ندارد. اگر مرحله دوم به نتیجه اول وابسته است، رابطه باید صریح باشد. خطای لغو هم نیاز به مسیر مناسب دارد. فقط حذف jQuery از این قسمت، رفتار محصول را به‌صورت خودکار معادل نمی‌کند.

در برخی بخش‌ها، CSS transition ساده‌تر است و مهاجرت به WAAPI لازم نیست. در برخی دیگر، حفظ منطق قدیمی تا زمان تعریف قرارداد جدید انتخاب منطقی‌تری است. هدف، کاهش وابستگی نامعلوم و کنترل روشن حرکت است. یک حرکت نمایشی نباید چند منبع state مستقل داشته باشد. اگر CSS و JavaScript همزمان همان property را تغییر می‌دهند، مالکیت و اولویت را مشخص کنید.

مدیریت چند حرکت و عمر مؤلفه

getAnimations می‌تواند حرکت‌های مربوط به عنصر را در اختیار بگذارد، ولی cancel کردن همه آن‌ها بدون شناخت مالکیت ممکن است اثر بخش دیگری را حذف کند. نگه داشتن reference همان حرکت، برای دامنه محدود روشن‌تر است. در مؤلفه‌ای که از DOM خارج می‌شود، listenerها و منابع مرتبط باید قرارداد cleanup داشته باشند. حذف عنصر از صفحه به معنی رسیدگی منطقی به همه Promiseهای برنامه نیست.

در React یا محیط دیگر، شروع حرکت باید با عمر مؤلفه هماهنگ باشد. reference ناپایدار یا closure قدیمی می‌تواند به state نامربوط اشاره کند. برای سؤال رفتار قابل مشاهده، راهنمای Testing Library محدودیت محیط DOM را توضیح می‌دهد. وجود یک animation object در آزمون، روان بودن یا خوانایی حرکت در دستگاه واقعی را ثابت نمی‌کند.

تحویل قرارداد حرکت

عنصر هدف، حالت پایه، نتیجه پایان و رفتار لغو را بنویسید. مشخص کنید شروع مجدد ادامه است یا ساخت چرخه تازه. زمان‌ها، تکرار و مسیر کاهش حرکت باید به همان markup مربوط باشند. برای پیام‌های نتیجه، راهنمای اعلان‌ها درباره بیان وضعیت واقعی کمک می‌کند. اگر movement تزئینی است، آن را به عنوان تنها نسخه خبر ارائه نکنید.

Web Animations API ابزار کنترل حرکت در مرورگر است. انتخاب مناسب آن از نیاز برنامه‌ای و قرارداد ظاهر می‌آید، نه از وعده بهترین عملکرد برای همه propertyها. نمونه مستقل کمک می‌کند pause، پایان و لغو از هم جدا دیده شوند. در پروژه واقعی، همین مرزها را با state، دسترس‌پذیری و عمر مؤلفه هماهنگ کنید تا حرکت مسیر کار را روشن‌تر کند و خودش منبع وضعیت مبهم نشود.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!