انتقالات عرض المستند نفسه لتطبيقات الصفحة الواحدة

تاريخ النشر: 17 آب (أغسطس) 2021، تاريخ آخر تعديل: 25 أيلول (سبتمبر) 2024

عند تشغيل انتقال عرض على مستند واحد، يطلق عليه انتقال عرض المستند نفسه. ويحدث ذلك عادةً في تطبيقات الصفحة الواحدة (SPA) التي يتم فيها استخدام JavaScript لتعديل DOM. أصبحت عمليات الانتقال بين طرق عرض المستند نفسه متاحة في Chrome اعتبارًا من الإصدار 111.

لبدء الانتقال إلى عرض المستند نفسه، يمكنك الاتصال بالرقم document.startViewTransition:

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

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

عند استدعاء هذه الوظيفة، يُسجِّل المتصفّح تلقائيًا لقطات لجميع العناصر التي تحتوي على خاصيّة view-transition-name CSS مُعلَن عنها.

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

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


الانتقال التلقائي: تلاشي متقاطع

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

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، وتقديم انتقالات مختلفة لها. ومع ذلك، في هذه الحالة، تم استخدام الانتقال التلقائي، وهو انتقال محوّل.

حسنًا، عملية النقل التلقائية ليست مجرد انتقال مموّه، بل إنّ ::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)
   └─ …

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

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

الآن، ينزلق نص العنوان قليلاً إلى الجانب لإفساح المجال لزر الرجوع.


إضافة مؤثرات متحركة إلى عناصر زائفة متعددة بالطريقة نفسها باستخدام view-transition-class

دعم المتصفح

  • Chrome: 125.
  • الحافة: 125.
  • Firefox: غير مدعوم.
  • معاينة تقنية Safari: متاحة.

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

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

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

يمكن استخدام view-transition-class في العناصر الزائفة لنقل العرض لتطبيق قاعدة النمط نفسها.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

يستفيد مثال البطاقات التالي من مقتطف CSS السابق. يتم تطبيق التوقيت نفسه على جميع البطاقات، بما في ذلك البطاقات التي أضفتها حديثًا، باستخدام أداة اختيار واحدة: html::view-transition-group(.card).

تسجيل العرض التقديمي لميزة "البطاقات" باستخدام view-transition-class، يتم تطبيق animation-timing-function نفسه على جميع البطاقات باستثناء البطاقات التي تمت إضافتها أو إزالتها.

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

بما أنّ انتقالات العرض تستند إلى الرسوم المتحرّكة في 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;
}

وهذا يعني أنّ الصورة المصغّرة تبقى في وسط العنصر مع توسيع العرض، ولكن يتم "إلغاء اقتصاص" الصورة الكاملة عند انتقالها من نسبة العرض إلى الارتفاع 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;
  }
}

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


التعامل مع أنماط متعددة لنقل العرض باستخدام أنواع نقل العرض

دعم المتصفح

  • Chrome: 125.
  • الحافة: 125
  • Firefox: غير مدعوم.
  • Safari: 18

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

تسجيل العرض التوضيحي للتقسيم على صفحات. ويتم اختيار انتقالات مختلفة استنادًا إلى الصفحة التي ستنتقل إليها.

لهذا، يمكنك استخدام أنواع انتقالات العرض، التي تسمح لك بتعيين نوع واحد أو أكثر لانتقال عرض نشط. على سبيل المثال، عند الانتقال إلى صفحة أعلى في تسلسل التقسيم على صفحات، استخدِم النوع forwards وعند الانتقال إلى صفحة أقل استخدِم النوع backwards. تكون هذه الأنواع نشطة فقط عند التقاط أو تنفيذ عملية انتقال، ويمكن تخصيص كل نوع من خلال CSS لاستخدام رسوم متحركة مختلفة.

لاستخدام الأنواع في عملية انتقال لعرض المستند نفسه، عليك تمرير types إلى الطريقة startViewTransition. للسماح بذلك، تقبل document.startViewTransition أيضًا كائنًا: update هي دالة الاستدعاء التي تعدِّل DOM، وtypes هي مصفوفة من الأنواع.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
.

للردّ على هذه الأنواع، استخدِم أداة الاختيار :active-view-transition-type(). نقْل type الذي تريد استهدافه إلى أداة الاختيار. وهذا يتيح لك الاحتفاظ بأنماط انتقالات طرق العرض المتعددة مفصولة عن بعضها البعض، بدون تداخل تعريفات أحد العروض مع تعريفات طرق العرض الأخرى.

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

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

في العرض التوضيحي التالي للتقسيم على صفحات، يتم تمرير محتوى الصفحة للأمام أو للخلف استنادًا إلى رقم الصفحة التي تنتقل إليها. يتم تحديد الأنواع عند النقر عليها، ويتم تمريرها إلى document.startViewTransition.

لاستهداف أي انتقال عرض نشط، بغض النظر عن نوعه، يمكنك استخدام أداة اختيار الفئة الصورية :active-view-transition بدلاً من ذلك.

html:active-view-transition {
    
}

التعامل مع أنماط انتقال متعددة لطرق العرض من خلال اسم فئة في جذر انتقال طريقة العرض

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

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

قبل أنواع الانتقال، كانت طريقة التعامل مع هذه الحالات هي ضبط اسم فئة مؤقتًا على جذر الانتقال. عند استدعاء document.startViewTransition، يكون جذر الانتقال هذا هو العنصر <html>، ويمكن الوصول إليه باستخدام document.documentElement في JavaScript:

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;
}

هذا كل ما في الأمر!

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

يتم الآن تشغيل الفيديو أثناء عملية النقل.


الدمج مع Navigation API (وإطارات العمل الأخرى)

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

في مقتطف الرمز التالي المأخوذ من هذا العرض التوضيحي للتقسيم على صفحات، يتم تعديل معالِج اعتراض واجهة برمجة تطبيقات التنقّل لطلب document.startViewTransition عندما يكون انتقالات العرض متاحة.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

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

ولذلك، يُنصح بمنع بدء انتقال طريقة العرض عندما يوفّر المتصفّح الانتقال المرئي الخاص به. لتحقيق ذلك، تحقّق من قيمة السمة hasUAVisualTransition لمثيل NavigateEvent. يتم ضبط السمة على true عندما يقدّم المتصفّح انتقالًا مرئيًا. تتوفّر السمة hasUIVisualTransition هذه أيضًا على PopStateEvent نسخة.

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

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

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

مقارنة الموقع الإلكتروني نفسه بدون فحص (لليسار) والعرض (لليسار) لـ hasUAVisualTransition

إضافة الحركة باستخدام 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 عندما "تستقر" طريقة العرض الجديدة، أي عندما يكتمل أي انتقال متحرك أو يتم تخطّيه إلى النهاية، استبدِل switchView بـ transition.finished.


ليس polyfill، ولكن…

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

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

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

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

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

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

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

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


العمل باستخدام أطر العمل

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

  • التفاعل: المفتاح هنا هو flushSync، والذي يطبِّق مجموعة من تغييرات الحالات بشكل متزامن. نعم، هناك تحذير كبير بشأن استخدام واجهة برمجة التطبيقات هذه، إلا أنّ دان أبراموف يؤكد لي أنّ ذلك مناسب في هذه الحالة. كما هو الحال مع React والرمز غير المتزامن، عند استخدام الوعود المختلفة التي يعرضها 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(update)

ابدأ ViewTransition جديد.

update هي دالة يتمّ استدعاؤها بعد تسجيل الحالة الحالية للمستند.

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

const viewTransition = document.startViewTransition({ update, types })

بدء ViewTransition جديد بالأنواع المحدّدة

يتم استدعاء update بعد تسجيل الحالة الحالية للمستند.

تعمل types على تحديد الأنواع النشطة لعملية النقل عند التقاط عملية الانتقال أو تنفيذها. يكون فارغًا في البداية. يُرجى الاطّلاع على viewTransition.types في الأسفل للحصول على مزيد من المعلومات.

أعضاء الأجهزة الافتراضية في ViewTransition:

viewTransition.updateCallbackDone

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

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

viewTransition.ready

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

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

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

viewTransition.finished

هو الوعد الذي يتحقق بمجرد ظهور الحالة النهائية بشكل كامل وتفاعلها للمستخدم.

ولا يتم الرفض إلا إذا أرجع updateCallback وعدًا مرفوضًا، لأنّ ذلك يشير إلى أنّه لم يتم إنشاء الحالة النهائية.

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

viewTransition.types

كائن يشبه Set يحتوي على أنواع انتقال العرض النشط. لإجراء تغييرات على الإدخالات، استخدِم طرق المثيل clear() وadd() وdelete().

للرد على نوع معيّن في CSS، استخدِم أداة اختيار الفئة الصورية :active-view-transition-type(type) على جذر الانتقال.

يتم تلقائيًا تنظيف الأنواع عند انتهاء عملية انتقال العرض.

viewTransition.skipTransition()

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

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


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

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

في مكان ممتاز.

انتقالات width وheight بين حالتَي "قبل" و"بعد"

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

::view-transition-image-pair

أنا في وضع مؤهّل تمامًا لملء المجموعة.

يجب أن يكون isolation: isolate للحد من تأثير mix-blend-mode على المشاهدات القديمة والجديدة.

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

يتم وضعها بشكل مطلق في أعلى يمين الغلاف.

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

تضم mix-blend-mode: plus-lighter لإتاحة تلاشي متقاطع حقيقي.

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


ملاحظات

يسرّنا دائمًا تلقّي ملاحظات المطوّر. لإجراء ذلك، يمكنك الإبلاغ عن مشكلة مع فريق عمل CSS على GitHub وتقديم اقتراحات وأسئلة. ابدأ مشكلتك باستخدام "[css-view-transitions]".

إذا واجهت خللاً، يُرجى إرسال تقرير عن خلل في Chromium بدلاً من ذلك.