الـ easing دالة، مش سحر
أي حركة ليها وقت t من 0 لـ 1 (البداية والنهاية). الـ easing دالة بتاخد t وترجّع قد إيه العنصر وصل، برضه من 0 لـ 1.
- Linear:
tزي ما هي. الحركة بسرعة ثابتة، فتحسها آلية. - CubicEaseOut: بتبدأ سريعة وتهدى في الآخر. زي عربية بتركن.
- EaseInCubic: بتبدأ بطيئة وتسرّع. Filee بيستخدمها في القفل بس.
- BackEaseOut: سريعة، تعدّي الهدف شعرة، وترجع. ده سر الطراوة.
في Filee فيه اتنين BackEaseOut مش واحد
ده اللي هتلاقيه لو قريت الكود كويس:
- الزراير والـ chips والـ sidebar في الـ XAML بتستخدم
<BackEaseOut />الجاهز بتاع Avalonia. - الدونت مش بيستخدمه. Filee كاتب
EaseOutBackبإيده جوهDonutMenu.csبرقم نطة هو اختاره.
الفرق كبير: الجاهز بتاع Avalonia بيعدّي الهدف بحوالي ٣٨٪. اللي في الدونت بيعدّيه بحوالي ٨.٥٪ بس.
طب ليه الزراير مش باينة بتترقص مع ٣٨٪؟ عشان المسافة صغيرة جدًا. الزرار بيروح من 1 لـ 0.95، يعني ٥٪ بس. ٣٨٪ من ٥٪ = أقل من ٢٪ من حجم الزرار. نطة صغيرة خالص، بتحسها ومتشوفهاش. الشريحة في الدونت بقى بتكبر من 0.55 لـ 1، فلو نطت ٣٨٪ كانت هتطير. عشان كده محتاجة رقم أهدى.
المعادلة اللي جوه الدونت
public override double Ease(double progress){ double p = 1d - progress; return 1 - p * (p * p - Math.Sin(p * Math.PI));}وده اللي Filee كاتبه للدونت:
private static double EaseOutBack(double t){ const double c1 = 1.55; // قوة النطة const double c3 = c1 + 1; return 1 + c3 * Math.Pow(t - 1, 3) + c1 * Math.Pow(t - 1, 2);}private static double EaseInCubic(double t) => t * t * t;الرقم المشهور في المعادلة دي هو 1.70158، ودي نطة ١٠٪ بالظبط. Filee اختار 1.55، فالنطة حوالي ٨.٥٪. نطة أصغر = طراوة من غير ما الحاجة تبان بتترقص.
جرّب بنفسك: حرّك الـ slider وشوف الـ c1 بيعمل إيه.
نرسم BackEaseOut على canvas بإيدنا
ده الكود كامل. صفحة HTML واحدة، افتحها في المتصفح وهتشوف المنحنى بيترسم وكورة بتتحرك عليه.
<canvas id="c" width="600" height="320" style="width:600px;height:320px"></canvas><button id="play">▶ شغّل</button><script> const canvas = document.getElementById("c"); const ctx = canvas.getContext("2d"); const W = 600, H = 320; const pad = 40; // نفس دالة Filee const c1 = 1.55; const backOut = (t) => 1 + (c1 + 1) * (t - 1) ** 3 + c1 * (t - 1) ** 2; // من (t, value) لبكسلات. القيمة من -0.1 لـ 1.3 عشان الـ overshoot يبان // (لو هترسم بتاعة Avalonia، كبّر الـ 1.3 لـ 1.45 لأنها بتطلع لحد 1.38) const X = (t) => pad + t * (W - pad * 2); const Y = (v) => pad + ((1.3 - v) / 1.4) * (H - pad * 2); function draw(progress) { ctx.clearRect(0, 0, W, H); // خط الهدف (value = 1) متقطّع ctx.setLineDash([6, 5]); ctx.strokeStyle = "#bbb"; ctx.beginPath(); ctx.moveTo(X(0), Y(1)); ctx.lineTo(X(1), Y(1)); ctx.stroke(); ctx.setLineDash([]); // المنحنى لحد الـ progress الحالي ctx.strokeStyle = "#6c4bff"; ctx.lineWidth = 3; ctx.beginPath(); for (let i = 0; i <= 100; i++) { const t = (i / 100) * progress; const x = X(t), y = Y(backOut(t)); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); } ctx.stroke(); // كورة في راس المنحنى ctx.fillStyle = "#6c4bff"; ctx.beginPath(); ctx.arc(X(progress), Y(backOut(progress)), 6, 0, Math.PI * 2); ctx.fill(); } function play() { const duration = 1200; // ms. بطيء بقصد عشان تتفرج let start = null; function frame(now) { start ??= now; const p = Math.min(1, (now - start) / duration); draw(p); if (p < 1) requestAnimationFrame(frame); // الفريم اللي بعده } requestAnimationFrame(frame); } document.getElementById("play").onclick = play; play();</script>نفس المنحنى في CSS
CSS معندوش back-out جاهز، بس cubic-bezier بيقرّبه كويس:
:root { --ease-back: cubic-bezier(0.34, 1.56, 0.64, 1); /* ≈ BackEaseOut */ --ease-out: cubic-bezier(0.33, 1, 0.68, 1); /* ≈ CubicEaseOut */ --ease-in: cubic-bezier(0.32, 0, 0.67, 0); /* ≈ EaseInCubic */}الرقم 1.56 في النص هو اللي بيخلي المنحنى يطلع فوق الـ 1، والنطة بتاعته حوالي ١٠٪. ده قريب من الدونت. لو عايز نطة أكبر للحاجات الصغيرة زي Avalonia، كبّر الرقم ده (مثلًا cubic-bezier(0.34, 2.2, 0.64, 1)). ولو عايز المعادلة بالظبط، استخدم linear() الجديدة في CSS بنقط كتير، أو اعمل الحركة بـ JavaScript.