عمليات انتقال سلسة وبسيطة باستخدام View Transitions API

التوافق مع المتصفح

  • 111
  • 111
  • x
  • x

المصدر

تُسهّل واجهة برمجة تطبيقات View Transition API تغيير نموذج العناصر في المستند (DOM) في خطوة واحدة، أثناء إنشاء انتقال متحرك بين الحالتين. تتوفّر هذه الميزة في الإصدار 111 من Chrome والإصدارات الأحدث.

عمليات نقل تم إنشاؤها باستخدام واجهة برمجة تطبيقات View Transition API. جرِّب الموقع الإلكتروني التجريبي: يتطلّب استخدام الإصدار 111 من Chrome أو الإصدارات الأحدث.

لماذا نحتاج إلى هذه الميزة؟

لا تبدو انتقالات الصفحة رائعة فحسب، بل تنقل أيضًا اتجاه التدفق، وتوضح العناصر المرتبطة من صفحة إلى أخرى. ويمكن أن تحدث حتى أثناء استرجاع البيانات، ما يؤدي إلى إدراك الأداء بشكل أسرع.

ولكن، لدينا حاليًا أدوات للصور المتحركة على الويب، مثل عناصر انتقال CSS والصور المتحركة في CSS وWeb Animation API، لذا لماذا نحتاج إلى شيء جديد لنقل العناصر؟

والحقيقة هي أن انتقالات الولايات صعبة، حتى باستخدام الأدوات التي لدينا بالفعل.

حتى شيء مثل تلاشي بسيط متقاطع يتضمن وجود كلتا الحالتين في نفس الوقت. يمثل هذا تحديات سهولة الاستخدام، مثل التعامل مع التفاعل الإضافي على العنصر الصادر. بالنسبة إلى مستخدمي الأجهزة المساعِدة، تكون هناك فترة تكون فيها الحالة قبل وبعد في نموذج العناصر في المستند (DOM) في الوقت نفسه، وقد تتحرك الأشياء في الشجرة بطريقة لا بأس بها من الناحية البصرية، ولكنها قد تؤدي بسهولة إلى فقدان موضع القراءة والتركيز.

يمثل التعامل مع تغييرات الحالة أمرًا صعبًا بشكل خاص إذا اختلفت حالتان في موضع التمرير. وإذا كان ينتقل عنصر من حاوية إلى أخرى، قد تواجه صعوبات في استخدام علامة overflow: hidden وأشكال اقتصاص أخرى، ما يعني أنّه عليك إعادة بنية CSS للحصول على التأثير المطلوب.

هذا ليس مستحيلاً، إنه صعب فقط.

تمنحك "عمليات النقل ضمن العرض" طريقة أسهل، من خلال السماح لك بإجراء تغيير DOM بدون أي تداخل بين الحالات، ولكن يمكنك إنشاء رسم متحرك لانتقال بين الحالات باستخدام طرق العرض التي تم التقاطها.

بالإضافة إلى ذلك، على الرغم من أنّ عملية التنفيذ الحالية تستهدف تطبيقات الصفحة الواحدة، سيتم توسيع هذه الميزة للسماح بالانتقال بين عمليات تحميل الصفحات الكاملة، وهو ما يستحيل حاليًا.

حالة توحيد المقاييس

ويجري تطوير الميزة ضمن مجموعة عمل CSS لـ W3C بصفتها مواصفات المسودة.

بمجرد أن نكون راضيًا عن تصميم واجهة برمجة التطبيقات، سنبدأ العمليات والفحوصات المطلوبة لشحن هذه الميزة إلى الوضع المستقر.

تُعد ملاحظات مطوِّري البرامج مهمة جدًا، لذا يُرجى الإبلاغ عن المشاكل على GitHub وتقديم الاقتراحات والأسئلة.

أبسط عناصر الانتقال: تأثير التلاشي المتقاطع

إنّ النقل التلقائي لطريقة العرض هو تلاشي متداخل، لذا فهو بمثابة مقدمة رائعة لواجهة برمجة التطبيقات:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

حيث يغيِّر updateTheDOMSomehow عنصر DOM إلى الحالة الجديدة. يمكن إجراء ذلك كيفما تريد: إضافة/إزالة العناصر، وتغيير أسماء الفئات، وتغيير الأنماط... لا يهم.

وبهذه الطريقة، تتلاشى الصفحات:

التلاشي المتداخل التلقائي. الحد الأدنى للعرض التوضيحي: المصدر.

حسنًا، التلاشي المتقاطع ليس مثيرًا للإعجاب. لحسن الحظ، يمكن تخصيص الانتقالات، ولكن قبل أن نصل إلى ذلك، نحتاج إلى فهم كيفية عمل هذا التلاشي المتقاطع الأساسي.

آلية عمل هذه الانتقالات

أخذ عينة التعليمات البرمجية أعلاه:

document.startViewTransition(() => updateTheDOMSomehow(data));

عند استدعاء الدالة .startViewTransition()، تسجِّل واجهة برمجة التطبيقات الحالة الحالية للصفحة. ويشمل ذلك أخذ لقطة شاشة.

بعد اكتمال هذه العملية، يتم استدعاء طلب معاودة الاتصال الذي تم تمريره إلى .startViewTransition(). وهذا هو المكان الذي يتغير فيه نموذج العناصر في المستند (DOM). وبعد ذلك، تسجّل واجهة برمجة التطبيقات الحالة الجديدة للصفحة.

وبعد الحصول على الحالة، تنشئ واجهة برمجة التطبيقات شجرة عنصر زائف على النحو التالي:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

يظهر عنصر ::view-transition فوق كلّ جزء في الصفحة. ويكون هذا مفيدًا إذا كنت تريد تعيين لون خلفية لعملية الانتقال.

::view-transition-old(root) هي لقطة شاشة لطريقة العرض القديمة، و::view-transition-new(root) هي تمثيل مباشر لطريقة العرض الجديدة. يتم عرض كليهما على شكل "محتوى مستبدل" في CSS (مثل <img>).

يتحرك العرض القديم من opacity: 1 إلى opacity: 0، بينما يتحرك العرض الجديد من opacity: 0 إلى opacity: 1، ما ينتج عنه تلاشي متداخل.

يتم تنفيذ جميع الرسوم المتحركة باستخدام رسوم CSS المتحركة، بحيث يمكن تخصيصها باستخدام CSS.

التخصيص البسيط

يمكن استهداف جميع العناصر الزائفة أعلاه باستخدام CSS، وبما أن الصور المتحركة يتم تحديدها باستخدام CSS، يمكنك تعديلها باستخدام خصائص صور CSS الحالية للصور المتحركة. مثال:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

باستخدام هذا التغيير، يصبح تلاشي الآن بطيئًا جدًا:

تلاشي متقاطع طويل. الحد الأدنى للعرض التوضيحي: المصدر.

حَسَنًا، مَا زَالَ الرَّأْيْ غَيْرْ مُرْتَفِعْ. بدلاً من ذلك، لن ننفذ الانتقال المحوري في Material Design:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

وإليك النتيجة:

انتقال المحور المشترك. الحد الأدنى للعرض التوضيحي: المصدر.

نقل عناصر متعددة

في العرض التوضيحي السابق، تشارك الصفحة بأكملها في انتقال المحور المشترك. ينطبق ذلك على معظم الصفحة، لكن لا يبدو مناسبًا تمامًا للعنوان، حيث ينزلق للداخل مرة أخرى.

ولتجنب ذلك، يمكنك استخراج العنوان من باقي الصفحة بحيث يمكن تحريكه بشكل منفصل. ويتم ذلك من خلال تحديد view-transition-name للعنصر.

.main-header {
  view-transition-name: main-header;
}

يمكن أن تكون قيمة view-transition-name أي قيمة تريدها (باستثناء none، ما يعني أنّه ما مِن اسم لعملية النقل). يتم استخدامه لتحديد العنصر بشكل فريد عبر عملية الانتقال.

ونتيجة ذلك:

انتقال المحور المشترك برأس ثابت. الحد الأدنى للعرض التوضيحي: المصدر.

الآن يبقى العنوان في مكانه ويتلاشى متقاطعًا.

تسبب إعلان CSS هذا في تغيير شجرة العنصر الزائف:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

هناك الآن مجموعتا نقل. واحد للرأس والآخر لبقية. ويمكن استهدافها بشكل مستقل باستخدام CSS، ومنحها انتقالات مختلفة. وعلى الرغم من ذلك، في هذه الحالة، يتم ترك main-header مع الانتقال التلقائي، وهو تلاشي متقاطع.

حسنًا، حسنًا، الانتقال التلقائي ليس مجرد تلاشي متقاطع، بل الانتقالات ::view-transition-group أيضًا:

  • الموضع والتحويل (عبر transform)
  • العرض
  • الطول

لم يكن ذلك مهمًا حتى الآن، لأنّ العنوان له نفس الحجم والموضع مع طرفَي تغيير DOM. ولكن يمكننا أيضًا استخراج النص الموجود في العنوان:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

يتم استخدام fit-content بحيث يكون العنصر بحجم النص، بدلاً من أن يمتد إلى العرض المتبقي. بدون ذلك، يقلل السهم الخلفي حجم عنصر نص العنوان، بينما نريد أن يكون بنفس الحجم في كلتا الصفحتين.

لدينا الآن ثلاثة أجزاء:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

ولكن مرة أخرى، ما عليك سوى استخدام الإعدادات الافتراضية:

تمرير نص العنوان: الحد الأدنى للعرض التوضيحي: المصدر.

الآن يقوم نص العنوان بتمرير مرضٍ بعض الشيء عبر لتوفير مساحة لزر الرجوع.

تصحيح أخطاء الانتقالات

نظرًا لإنشاء "انتقالات العرض" فوق الصور المتحركة في CSS، تُعدّ لوحة الصور المتحركة في "أدوات مطوري البرامج في Chrome" رائعة لتصحيح أخطاء الانتقالات.

باستخدام لوحة الصور المتحركة، يمكنك إيقاف الصورة المتحركة التالية مؤقتًا، ثم استعراضها للأمام والخلف. خلال ذلك، يمكن العثور على العناصر الزائفة الانتقالية في لوحة العناصر.

تصحيح الأخطاء في عمليات نقل طرق العرض باستخدام "أدوات مطوّري البرامج في Chrome"

لا يلزم أن تكون العناصر الانتقالية عنصر DOM نفسه

استخدمنا حتى الآن view-transition-name لإنشاء عناصر انتقال منفصلة للرأس والنص في العنوان. وهي من الناحية النظرية هي العنصر نفسه قبل تغيير DOM وبعده، ولكن يمكنك إنشاء انتقالات عندما لا يكون الأمر كذلك.

على سبيل المثال، يمكن إضافة view-transition-name إلى الفيديو المضمّن الرئيسي:

.full-embed {
  view-transition-name: full-embed;
}

بعد ذلك، عند النقر على الصورة المصغّرة، يمكن منحها القيمة نفسها view-transition-name، وذلك خلال مدة الانتقال فقط:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

والنتيجة:

ينتقل أحد العناصر إلى عنصر آخر. الحد الأدنى للعرض التوضيحي: المصدر.

تنتقل الصورة المصغّرة الآن إلى الصورة الرئيسية. على الرغم من أنّهما عناصر مختلفة من الناحية النظرية (وحرفيًا)، تتعامل واجهة برمجة التطبيقات الخاصة بالانتقال مع هذه العناصر على أنّها العنصر نفسه لأنّهما يتشاركان عنصر view-transition-name نفسه.

وتُعد الكود الحقيقي لهذا الأمر أكثر تعقيدًا بعض الشيء من المثال البسيط أعلاه، كما أنه يعالج أيضًا الانتقال مرة أخرى إلى صفحة الصورة المصغرة. اطّلِع على المصدر لمعرفة عملية التنفيذ الكاملة.

عمليات الانتقال والخروج المخصّصة

انظر إلى هذا المثال:

الدخول إلى الشريط الجانبي والخروج منه: الحد الأدنى للعرض التوضيحي: المصدر.

الشريط الجانبي هو جزء من عملية الانتقال:

.sidebar {
  view-transition-name: sidebar;
}

ولكن على عكس العنوان في المثال السابق، لا يظهر الشريط الجانبي في جميع الصفحات. إذا تضمّنت كلتا الحالتين شريطًا جانبيًا، ستبدو العناصر الانتقالية الانتقالية على النحو التالي:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

في المقابل، إذا كان الشريط الجانبي على الصفحة الجديدة فقط، لن يظهر العنصر ::view-transition-old(sidebar) الزائف. بسبب عدم توفّر صورة "قديمة" للشريط الجانبي، سيحتوي زوج الصور على ::view-transition-new(sidebar) فقط. وبالمثل، إذا كان الشريط الجانبي على الصفحة القديمة فقط، سيحتوي زوج الصور على ::view-transition-old(sidebar) فقط.

في العرض التوضيحي أعلاه، ينتقل الشريط الجانبي بشكل مختلف بناءً على ما إذا كان يدخل أو يخرج أو موجودًا في كلتا الحالتين. يدخل النموذج عن طريق الانزلاق من اليمين ثم يتلاشى للداخل، ويخرج من خلال التمرير إلى اليمين ويتلاشى، ويظل في مكانه عندما يكون موجودًا في كلتا الحالتين.

لإنشاء انتقالات محدَّدة للانتقال والخروج، يمكنك استخدام الفئة الزائفة :only-child لاستهداف العنصر الزائف القديم/الجديد عندما يكون العنصر الثانوي الوحيد في زوج الصور:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

في هذه الحالة، ليس هناك انتقال محدد عند وجود الشريط الجانبي في كلتا الحالتين، لأن الوضع الافتراضي هو الوضع المثالي.

تحديثات DOM غير المتزامنة، وفي انتظار المحتوى

يمكن أن تعرض معاودة الاتصال التي تم ضبطها إلى .startViewTransition() وعدًا، ما يسمح بتحديثات DOM غير المتزامنة، والانتظار إلى أن يصبح المحتوى المهم جاهزًا.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

لن تبدأ عملية النقل إلا بعد الوفاء بالوعد الذي منحته. وخلال هذا الوقت، تكون الصفحة مجمّدة، لذا يجب الحدّ من حالات التأخير هنا. على وجه التحديد، يجب إجراء عمليات استرجاع الشبكة قبل طلب خدمة .startViewTransition()، علمًا أنّ الصفحة لا تزال تفاعلية بالكامل، بدلاً من إجرائها كجزء من معاودة الاتصال بـ .startViewTransition().

إذا قررت الانتظار حتى تصبح الصور أو الخطوط جاهزة، فتأكد من استخدام مهلة صارمة:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

ومع ذلك، في بعض الحالات يكون من الأفضل تجنب التأخير تمامًا واستخدام المحتوى الموجود لديك من قبل.

الاستفادة إلى أقصى حد من المحتوى المتوفّر لديك

في حال انتقال الصورة المصغّرة إلى صورة أكبر:

الانتقال التلقائي هو تلاشي متقاطع، مما يعني أن الصورة المصغّرة يمكن أن تتداخل مع الصورة مع صورة كاملة لم يتم تحميلها بعد.

إحدى طرق التعامل مع هذا الأمر هي انتظار تحميل الصورة بالكامل قبل بدء عملية الانتقال. من المفترض أن يتم ذلك قبل طلب البيانات من .startViewTransition()، بحيث تظل الصفحة تفاعلية، ويمكن عرض مؤشر سريان العمل لإعلام المستخدمين بأنّه يتم تحميل الصفحة. ولكن في هذه الحالة، هناك طريقة أفضل:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

لا تتلاشى الصورة المصغّرة الآن، بل تظهر فقط تحت الصورة الكاملة. ويعني ذلك أنّه إذا لم يتم تحميل طريقة العرض الجديدة، ستكون الصورة المصغّرة ظاهرة خلال عملية الانتقال. وهذا يعني أن الانتقال يمكن أن يبدأ على الفور، ويمكن تحميل الصورة الكاملة في وقتها الخاص.

لن ينجح هذا الأمر إذا كان العرض الجديد يعرض الشفافية، لكننا نعلم في هذه الحالة أنه ليس كذلك، لذا يمكننا إجراء هذا التحسين.

التعامل مع التغييرات في نسبة العرض إلى الارتفاع

من الملائم، أن جميع الانتقالات حتى الآن تمت إلى عناصر بنفس نسبة العرض إلى الارتفاع، لكن هذا لن يكون هو الحال دائمًا. ماذا لو كانت الصورة المصغّرة 1:1 وكانت الصورة الرئيسية 16:9؟

ينتقل أحد العناصر إلى عنصر آخر مع تغيير نسبة العرض إلى الارتفاع. الحد الأدنى للعرض التوضيحي: المصدر.

في عملية الانتقال الافتراضية، تتحرك المجموعة من الحجم السابق إلى الحجم التالي. يتم عرض المجموعة القديمة والجديدة بنسبة 100%، مع ضبط الارتفاع التلقائي، ما يعني احتفاظهما بنسبة العرض إلى الارتفاع بغض النظر عن حجم المجموعة.

وهذا أمر افتراضي جيد، إلا أنه ليس ما نريده في هذه الحالة. وبالتالي:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

وهذا يعني أن الصورة المصغّرة تبقى في وسط العنصر مع توسع العرض، في حين أن الصورة الكاملة "un-اقتصاص" أثناء انتقالها من 1:1 إلى 16:9.

تغيير الانتقال بناءً على حالة الجهاز

قد ترغب في استخدام انتقالات مختلفة على الهاتف المحمول مقابل سطح المكتب، مثل هذا المثال الذي يؤدي شريحة كاملة من الجانب على الهاتف المحمول، ولكن شريحة أكثر دقة على سطح المكتب:

ينتقل أحد العناصر إلى عنصر آخر. الحد الأدنى للعرض التوضيحي: المصدر.

ويمكن تحقيق ذلك باستخدام استعلامات الوسائط العادية:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

ويمكنك أيضًا تغيير العناصر التي تريد تعيينها view-transition-name وفقًا للاستعلامات عن الوسائط المطابقة.

التفاعل مع الخيار المفضّل "الحركة المنخفضة"

يمكن أن يشير المستخدمون إلى أنّهم يفضّلون الحركة المنخفضة عبر نظام التشغيل، وأنّ هذا الخيار المفضّل يتم عرضه عبر CSS.

يمكنك منع أي عمليات نقل لهؤلاء المستخدمين:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

مع ذلك، فإنّ تفضيل "الحركة المنخفضة" لا يعني أنّ المستخدم لا يريد أي حركة. بدلاً مما سبق، يمكنك اختيار رسوم متحركة أكثر دقة، ولكن لا تزال تعبر عن العلاقة بين العناصر وتدفق البيانات.

تغيير الانتقال بناءً على نوع التنقل

في بعض الأحيان، يجب أن يحتوي التنقل من نوع معين من الصفحات إلى آخر على انتقال مخصص بشكل خاص. أو، يجب أن يكون التنقل "للخلف" مختلفًا عن التنقل "للأمام".

انتقالات مختلفة عند الرجوع: الحد الأدنى للعرض التوضيحي: المصدر.

وأفضل طريقة للتعامل مع هذه الحالات هي تحديد اسم فئة على <html> يُعرَف أيضًا باسم عنصر المستند:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

يستخدم هذا المثال السمة transition.finished، وهي وعد يتم التعامل معه بعد الوصول إلى حالته النهائية. وتتوفر الخصائص الأخرى لهذا الكائن في مرجع واجهة برمجة التطبيقات.

يمكنك الآن استخدام اسم الفئة في خدمة CSS لتغيير عملية النقل:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

وكما هو الحال مع طلبات البحث عن الوسائط، يمكن أيضًا استخدام هذه الفئات لتغيير العناصر التي تحصل على view-transition-name.

الانتقال بدون تجميد الرسوم المتحركة الأخرى

ألقِ نظرة على هذا العرض التوضيحي حول موضع انتقال الفيديو:

نقل الفيديو: الحد الأدنى للعرض التوضيحي: المصدر.

هل رأيت أي خطأ في ذلك؟ لا تقلق إذا لم تكن قد فعلت ذلك. في هذه الحالة، يتم إبطاء وتيرة السرعة:

عملية نقل الفيديو أبطأ: الحد الأدنى للعرض التوضيحي: المصدر.

وخلال الفترة الانتقالية، يبدو أنّ الفيديو يتجمّد، ثم يتلاشى إصدار الفيديو الذي يتم تشغيله. ويرجع ذلك إلى أنّ ::view-transition-old(video) هي لقطة شاشة لطريقة العرض القديمة، بينما ::view-transition-new(video) هي صورة مباشرة لطريقة العرض الجديدة.

يمكنك إصلاح ذلك، ولكن اسأل نفسك أولاً عما إذا كان الأمر يستحق الإصلاح. وإذا لم تظهر لك "المشكلة" عندما كان الانتقال يعمل بالسرعة الطبيعية، لما كنت لن أزعج نفسك بتغييره.

وإذا كنت تريد إصلاحها حقًا، يُرجى عدم عرض ::view-transition-old(video) والتبديل مباشرةً إلى ::view-transition-new(video). يمكنك القيام بذلك عن طريق إلغاء الأنماط والرسوم المتحركة الافتراضية:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

وهكذا انتهى كل شيء!

عملية نقل الفيديو أبطأ: الحد الأدنى للعرض التوضيحي: المصدر.

أما الآن، فيتم تشغيل الفيديو طوال فترة الانتقال.

الرسوم المتحركة باستخدام JavaScript

حتى الآن، تم تحديد جميع عمليات الانتقال باستخدام CSS، لكن في بعض الأحيان لا تكون CSS كافية:

نقل الدائرة: الحد الأدنى للعرض التوضيحي: المصدر.

هناك جزءان من هذا الانتقال لا يمكن تحقيقه باستخدام خدمة مقارنة الأسعار (CSS) وحدها:

  • تبدأ الرسوم المتحركة من موقع النقر.
  • تنتهي الرسوم المتحركة بدائرة لها نصف قطر في أبعد زاوية. ونأمل أن يصبح ذلك ممكنًا مع CSS في المستقبل.

لحسن الحظ، يمكنك إنشاء انتقالات باستخدام Web Animation API!

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

يستخدم هذا المثال السمة transition.ready، وهي وعد يتم حله بعد إنشاء العناصر الانتقالية الانتقالية بنجاح. وتتوفر الخصائص الأخرى لهذا الكائن في مرجع واجهة برمجة التطبيقات.

الانتقالات كتحسين

تم تصميم واجهة برمجة تطبيقات View Transition API "لتضمين" تغيير في نموذج العناصر في المستند (DOM) وإنشاء عملية انتقال له. ومع ذلك، يجب التعامل مع عملية النقل كتحسين، كما هو الحال مع تطبيقك، أنّه يجب عدم إدخال حالة "خطأ" إذا تم تغيير نموذج العناصر في المستند (DOM) بنجاح، ولكن تعذّر الانتقال. من المفترض ألا تفشل عملية النقل، ولكن إذا حدث ذلك، فيجب ألا تعطّل بقية تجربة المستخدم.

من أجل التعامل مع الانتقالات كتحسين، احرص على عدم استخدام وعود النقل بطريقة قد تؤدي إلى إيقاف تطبيقك في حالة فشل الانتقال.

الإجراءات غير المُوصى بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

المشكلة في هذا المثال هي أنّه سيتم رفض switchView() إذا تعذّر على الانتقال إلى الحالة ready، لكن هذا لا يعني تعذّر تبديل العرض. يُحتمَل أنّه تم تعديل نموذج العناصر في المستند (DOM) بنجاح، ولكن تم رصد عناصر view-transition-name مكرّرة، لذلك تم تخطّي عملية النقل.

يمكنك بدلاً من ذلك إجراء الخطوات التالية:

الإجراءات التي يُنصح بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

يستخدم هذا المثال transition.updateCallbackDone لانتظار تحديث DOM والرفض في حال تعذّر التحديث. لن يتم رفض switchView بعد الآن في حال تعذُّر عملية النقل، ويتم حلها عند اكتمال تحديث DOM، ويتم الرفض في حال تعذّر النقل.

إذا كنت تريد أن تتم معالجة switchView عند اكتمال العرض الجديد، كما هو الحال في أي عملية نقل متحرّكة قد اكتملت أو تم تخطيها إلى النهاية، يمكنك استبدال transition.updateCallbackDone بـ transition.finished.

ليس polyfill، ولكن...

لا أعتقد أنه يمكن إضافة هذه الميزة بأي طريقة مفيدة، ولكن يسعدني أن أثبت خطأ!

ومع ذلك، تُسهِّل هذه الدالة المساعدة الأمور بشكل كبير في المتصفحات التي لا تتيح الانتقالات بين طرق العرض:

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

ويمكن استخدامه على النحو التالي:

function spaNavigate(data) {
  const classNames = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

في المتصفحات التي لا تتيح عمليات نقل العرض، سيستمر استدعاء updateDOM، ولكن لن يكون هناك انتقال متحرك.

ويمكنك أيضًا توفير بعض classNames لإضافتها إلى <html> أثناء عملية الانتقال، ما يُسهِّل تغيير عملية النقل استنادًا إلى نوع التنقّل.

يمكنك أيضًا تمرير true إلى "skipTransition" إذا كنت لا تريد صورة متحركة، حتى في المتصفّحات التي تتيح "عمليات نقل العرض". ويكون هذا الأمر مفيدًا إذا كان هناك مستخدم مفضّل في موقعك الإلكتروني لإيقاف عمليات الانتقال.

العمل على أطر العمل

إذا كنت تعمل مع مكتبة أو إطار عمل يزيل تغييرات DOM، فإن الجزء المعقّد هو معرفة وقت اكتمال تغيير DOM. إليك مجموعة من الأمثلة باستخدام أداة المساعدة أعلاه في أُطر عمل مختلفة.

  • التفاعل: المفتاح هنا هو flushSync، الذي يطبّق مجموعة من تغييرات الحالة بشكل متزامن. نعم، هناك تحذير كبير بشأن استخدام واجهة برمجة التطبيقات هذه، إلا أنّ دان أبراموف يؤكد لي أنّ ذلك مناسب في هذه الحالة. عند استخدام الوعود المختلفة التي يعرضها startViewTransition، يجب التأكّد من أنّ الرمز يعمل بالحالة الصحيحة، وذلك كالعادة عند استخدام رمز التفاعل والتفاعل.
  • Vue.js: المفتاح هنا هو nextTick، ويتم تنفيذه بعد تعديل DOM.
  • Svelte: يشبه إلى حد كبير Vue، إلا أنّ طريقة انتظار التغيير التالي هي tick.
  • Lit: هو وعد this.updateComplete في المكوّنات، ويتم تنفيذه بعد تعديل DOM.
  • Angular: المفتاح هنا هو applicationRef.tick، والذي يعمل على مسح تغييرات DOM المعلّقة. اعتبارًا من الإصدار 17 من Angular، يمكنك استخدام withViewTransitions الذي يأتي مع @angular/router.

مرجع واجهة برمجة التطبيقات

const viewTransition = document.startViewTransition(updateCallback)

بدء ViewTransition جديد

يتم استدعاء updateCallback بعد الحصول على الحالة الحالية للمستند.

بعد ذلك، عند تنفيذ الوعد الذي تم إرجاعه من خلال "updateCallback"، تبدأ عملية النقل في الإطار التالي. في حال تم رفض الوعد الذي قدَّمه updateCallback، يتم إلغاء عملية النقل.

أعضاء مثال ViewTransition:

viewTransition.updateCallbackDone

يتم تنفيذ الوعد عند تنفيذه من قِبل "updateCallback"، أو يتم رفضه عند رفضه.

تغطي واجهة برمجة التطبيقات View Transition API تغييرًا في نموذج العناصر في المستند (DOM) وتنشئ عملية انتقال. ومع ذلك، في بعض الأحيان لا تهتم بنجاح/فشل الرسوم المتحركة للانتقال، فأنت تريد فقط معرفة ما إذا كان تغيير DOM ومتى حدث. إنّ السمة updateCallbackDone مخصّصة لحالة الاستخدام هذه.

viewTransition.ready

وعد يفي بمجرد إنشاء العناصر الزائفة للانتقال، وتكون الرسوم المتحركة على وشك البدء.

ويتم رفضها في حال تعذّر بدء عملية النقل. قد يرجع ذلك إلى خطأ في الإعداد، مثل إنشاء قيم view-transition-name مكرّرة أو عرض updateCallback بوعد مرفوض.

ويكون هذا مفيدًا في تحريك العناصر الصورية الانتقالية باستخدام JavaScript.

viewTransition.finished

وعد يتم الوفاء به بمجرد أن تصبح الحالة النهائية مرئية بالكامل وتفاعلية للمستخدم.

يتم رفض العملية فقط في حال عرض updateCallback بوعد مرفوض، لأنّه يشير إلى عدم إنشاء الحالة النهائية.

وبخلاف ذلك، في حال تعذّر بدء عملية النقل أو تم تخطيها أثناء عملية النقل، سيستمر الوصول إلى حالة النهاية، وبالتالي يتم تنفيذ finished.

viewTransition.skipTransition()

يمكنك تخطي جزء الحركة الانتقالية.

لن يؤدي ذلك إلى تخطّي طلب updateCallback، لأنّ تغيير DOM يكون منفصلاً عن عملية الانتقال.

مرجع النمط التلقائي والانتقال

::view-transition
العنصر الصوري الجذري الذي يملأ إطار العرض ويحتوي على كل ::view-transition-group.
::view-transition-group

في موضع قائم على الاستمرارية:

يتم الانتقال إلى width وheight بين الولايتَين "قبل" و"بعد".

الانتقالات transform بين رباعي مسافة إطار العرض "قبل" و"بعد"

::view-transition-image-pair

هو في وضع قوي لملء المجموعة.

تحتوي على isolation: isolate للحدّ من تأثير وضع المزج plus-lighter على طرق العرض القديمة والجديدة.

::view-transition-new و::view-transition-old

يتم وضعها تمامًا في أعلى يسار الغلاف.

يملأ العرض 100% من عرض المجموعة، ولكن له ارتفاع تلقائي، لذلك سيحافظ على نسبة العرض إلى الارتفاع بدلاً من ملء المجموعة.

تحتوي على mix-blend-mode: plus-lighter للسماح بتلاشي متداخل فعلي.

تنتقل طريقة العرض القديمة من opacity: 1 إلى opacity: 0. تنتقل طريقة العرض الجديدة من opacity: 0 إلى opacity: 1.

إضافة ملاحظات

تُعد ملاحظات مطوِّري البرامج مهمة حقًا في هذه المرحلة، لذا يُرجى الإبلاغ عن المشاكل على GitHub وتقديم الاقتراحات والأسئلة.