الأساس · فصل ٢

الـ Easing و BackEaseOut — ونرسمه على canvas

easing · BackEaseOut · overshoot · cubic-bezier · canvas · requestAnimationFrame
🎯 الهدف: تفهم الـ easing كدالة من الوقت للقيمة، وتكتب BackEaseOut بإيدك وترسمه على canvas.

الـ easing دالة، مش سحر

أي حركة ليها وقت t من 0 لـ 1 (البداية والنهاية). الـ easing دالة بتاخد t وترجّع قد إيه العنصر وصل، برضه من 0 لـ 1.

  • Linear: t زي ما هي. الحركة بسرعة ثابتة، فتحسها آلية.
  • CubicEaseOut: بتبدأ سريعة وتهدى في الآخر. زي عربية بتركن.
  • EaseInCubic: بتبدأ بطيئة وتسرّع. Filee بيستخدمها في القفل بس.
  • BackEaseOut: سريعة، تعدّي الهدف شعرة، وترجع. ده سر الطراوة.
المنحنيات الأربعة جنب بعض، والكورة تحت كل منحنى بتتحرك بنفس القيمة
Linear
EaseInCubic
CubicEaseOut
EaseOutBack 1.55

في Filee فيه اتنين BackEaseOut مش واحد

ده اللي هتلاقيه لو قريت الكود كويس:

  • الزراير والـ chips والـ sidebar في الـ XAML بتستخدم <BackEaseOut /> الجاهز بتاع Avalonia.
  • الدونت مش بيستخدمه. Filee كاتب EaseOutBack بإيده جوه DonutMenu.cs برقم نطة هو اختاره.

الفرق كبير: الجاهز بتاع Avalonia بيعدّي الهدف بحوالي ٣٨٪. اللي في الدونت بيعدّيه بحوالي ٨.٥٪ بس.

الاتنين جنب بعض: البينك بتاع Avalonia، والبنفسجي بتاع الدونت
CubicEaseOut
Avalonia BackEaseOut
EaseOutBack 1.55

طب ليه الزراير مش باينة بتترقص مع ٣٨٪؟ عشان المسافة صغيرة جدًا. الزرار بيروح من 1 لـ 0.95، يعني ٥٪ بس. ٣٨٪ من ٥٪ = أقل من ٢٪ من حجم الزرار. نطة صغيرة خالص، بتحسها ومتشوفهاش. الشريحة في الدونت بقى بتكبر من 0.55 لـ 1، فلو نطت ٣٨٪ كانت هتطير. عشان كده محتاجة رقم أهدى.

المعادلة اللي جوه الدونت

Avalonia: Easings/BackEaseOut.cs
public override double Ease(double progress){    double p = 1d - progress;    return 1 - p * (p * p - Math.Sin(p * Math.PI));}

وده اللي Filee كاتبه للدونت:

Controls/DonutMenu.cs (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 بيعمل إيه.

غيّر c1: صفر = من غير نطة، 1.55 = Filee، 3 = مبالغة
CubicEaseOut
EaseOutBack 1.55

نرسم BackEaseOut على canvas بإيدنا

ده الكود كامل. صفحة HTML واحدة، افتحها في المتصفح وهتشوف المنحنى بيترسم وكورة بتتحرك عليه.

back-ease-out.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 بيقرّبه كويس:

soft.css
: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.

خلّصت الفصل لما

🧠 اختبر نفسك
١) يعني إيه overshoot؟
إن القيمة تعدّي الهدف (أكبر من 1) شوية وترجع له. ده اللي بيدّي إحساس الطراوة.
٢) لو c1 = 0 في EaseOutBack، إيه اللي يحصل؟
المعادلة تبقى 1 + (t-1)³، ودي CubicEaseOut بالظبط. مفيش نطة خالص.
٣) ليه الزراير بتستحمل BackEaseOut بتاع Avalonia (٣٨٪) والدونت لأ؟
لأن الزرار بيتحرك من 1 لـ 0.95 بس، فالنطة بتطلع أقل من ٢٪ من حجمه. الشريحة بتتحرك مسافة أكبر بكتير، فمحتاجة نطة أصغر (٨.٥٪).
٤) ليه Filee بيستخدم EaseInCubic في القفل؟
بتبدأ بطيئة وتخلص بسرعة، فالشريحة “بتتسحب” لجوه من غير ما تنط. مفيش overshoot في القفل.