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

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

لبدء الانتقال إلى عرض المستند نفسه، يمكنك الاتصال بالرقم 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());
}

عند الاستدعاء، يلتقط المتصفّح تلقائيًا لقطات لجميع العناصر التي تتضمّن سمة CSS view-transition-name تم تعريفها عليها.

بعد ذلك، تنفّذ الدالة التي تم تمريرها في عملية الاستدعاء التي تعدِّل 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، وإدخال انتقالات مختلفة. على الرغم من أنه في هذه الحالة، تم ترك 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)
   └─ …

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

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

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


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

دعم المتصفح

  • 125
  • 125
  • x
  • x

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

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

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


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

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

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

لهذا، يمكنك استخدام أنواع انتقالات العرض، التي تسمح لك بتعيين نوع واحد أو أكثر لانتقال عرض نشط. على سبيل المثال، عند الانتقال إلى صفحة أعلى في تسلسل التقسيم على صفحات، استخدِم النوع 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;
}

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

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

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


الصور المتحركة باستخدام 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,
  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 بدلاً من ذلك.