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

عندما يتم تشغيل انتقال عرض على مستند واحد، يُعرف ذلك باسم انتقال عرض المستند نفسه. ينطبق هذا الأمر عادةً في تطبيقات الصفحة الواحدة (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، وإدخال انتقالات مختلفة. ومع ذلك، في هذه الحالة، تم استخدام الانتقال التلقائي، وهو انتقال محوّل.

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

وهذا يعني أن الصورة المصغرة تبقى في وسط العنصر مع توسع العرض، ولكن الصورة الكاملة "un-crops" عند انتقاله من 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: غير متاح

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

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

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