انتقال صاف و ساده با View Transitions API

پشتیبانی مرورگر

  • 111
  • 111
  • ایکس
  • ایکس

منبع

View Transition API تغییر DOM را در یک مرحله آسان می کند، در حالی که یک انتقال متحرک بین دو حالت ایجاد می کند. در Chrome 111+ موجود است.

انتقال ایجاد شده با View Transition API. سایت آزمایشی را امتحان کنید - به Chrome 111+ نیاز دارد.

چرا به این ویژگی نیاز داریم؟

انتقال های صفحه نه تنها عالی به نظر می رسند، بلکه جهت جریان را نیز به اشتراک می گذارند و روشن می کنند که کدام عناصر از صفحه ای به صفحه دیگر مرتبط هستند. آنها حتی می توانند در حین واکشی داده ها اتفاق بیفتند که منجر به درک سریعتر عملکرد می شود.

اما، ما در حال حاضر ابزارهای انیمیشن را در وب داریم، مانند انتقال CSS ، انیمیشن‌های CSS ، و Web Animation API ، پس چرا به یک چیز جدید برای جابجایی چیزها نیاز داریم؟

حقیقت این است که انتقال وضعیت سخت است، حتی با ابزارهایی که در حال حاضر داریم.

حتی چیزی مانند یک متقاطع محو ساده شامل حضور هر دو حالت به طور همزمان است. این چالش‌های قابلیت استفاده را ایجاد می‌کند، مانند مدیریت تعامل اضافی در عنصر خروجی. همچنین، برای کاربران دستگاه‌های کمکی، دوره‌ای وجود دارد که هر دو حالت قبل و بعد به طور همزمان در DOM هستند، و ممکن است اشیاء در اطراف درخت به‌گونه‌ای حرکت کنند که از نظر بصری خوب باشد، اما به راحتی می‌تواند موقعیت خواندن و تمرکز را ایجاد کند. گم شده.

اگر این دو حالت در موقعیت اسکرول متفاوت باشند، رسیدگی به تغییرات حالت به ویژه چالش برانگیز است. و اگر یک عنصر در حال حرکت از یک ظرف به ظرف دیگر باشد، می‌توانید با مشکلاتی در overflow: hidden و سایر اشکال بریده شدن، به این معنی که باید CSS خود را بازسازی کنید تا به جلوه‌ای که می‌خواهید برسید.

غیرممکن نیست، فقط واقعا سخت است.

View Transitions راه آسان‌تری را در اختیار شما قرار می‌دهد، به این ترتیب که به شما امکان می‌دهد DOM خود را بدون همپوشانی بین حالت‌ها تغییر دهید، اما با استفاده از نماهای فوری، یک انیمیشن انتقال بین حالت‌ها ایجاد کنید.

علاوه بر این، اگرچه اجرای فعلی برنامه‌های تک صفحه‌ای (SPA) را هدف قرار می‌دهد، این ویژگی برای انتقال بین بارگذاری کامل صفحه گسترش می‌یابد که در حال حاضر غیرممکن است.

وضعیت استانداردسازی

این ویژگی در گروه کاری W3C CSS به عنوان پیش نویس مشخصات در حال توسعه است.

هنگامی که از طراحی API راضی بودیم، فرآیندها و بررسی های لازم برای ارسال این ویژگی را به حالت پایدار آغاز می کنیم.

بازخورد برنامه‌نویس واقعاً مهم است، بنابراین لطفاً مشکلات را با پیشنهادات و سؤالات در GitHub ارسال کنید .

ساده ترین انتقال: متقاطع محو شدن

پیش‌فرض View Transition یک cross-fade است، بنابراین به عنوان یک مقدمه خوب برای API عمل می‌کند:

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() فراخوانی می شود، API وضعیت فعلی صفحه را ثبت می کند. این شامل گرفتن اسکرین شات نیز می شود.

پس از تکمیل، تماس ارسال شده به .startViewTransition() فراخوانی می شود. اینجاست که DOM تغییر می کند. سپس، API وضعیت جدید صفحه را ثبت می کند.

هنگامی که حالت ضبط می شود، API یک درخت شبه عنصر مانند زیر می سازد:

::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 transition نیز انجام می‌شود:

  • موقعیت و تبدیل (از طریق 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 Transitions بر روی انیمیشن‌های CSS ساخته شده‌اند، پانل انیمیشن‌ها در Chrome DevTools برای اشکال‌زدایی انتقال‌ها عالی است.

با استفاده از پانل انیمیشن ها ، می توانید انیمیشن بعدی را مکث کنید، سپس انیمیشن را به جلو و عقب بکشید. در طی این، شبه عناصر انتقال را می توان در پنل Elements یافت.

اشکال زدایی انتقال مشاهده با ابزارهای توسعه دهنده کروم.

لازم نیست عناصر انتقالی همان عنصر 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();
  });
};

و نتیجه:

یک عنصر در حال انتقال به عنصر دیگر حداقل نسخه ی نمایشی منبع .

تصویر کوچک اکنون به تصویر اصلی تبدیل می شود. اگرچه آنها از نظر مفهومی (و به معنای واقعی کلمه) عناصر متفاوتی هستند، API انتقال آنها را به عنوان یک چیز در نظر می گیرد زیرا آنها 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)]);
});

با این حال، در برخی موارد بهتر است به طور کلی از تاخیر جلوگیری کنید و از محتوایی که قبلا دارید استفاده کنید.

از محتوایی که در حال حاضر دارید حداکثر استفاده را ببرید

در موردی که تصویر کوچک به یک تصویر بزرگتر تبدیل می شود:

انتقال پیش‌فرض روی cross-fade است، به این معنی که تصویر کوچک می‌تواند با یک تصویر کامل که هنوز بارگذاری نشده است، متقاطع محو شود.

یکی از راه‌های رسیدگی به این موضوع این است که قبل از شروع انتقال صبر کنید تا تصویر کامل بارگذاری شود. در حالت ایده‌آل این کار قبل از فراخوانی .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;
  }
}

با این حال، ترجیح برای "حرکت کاهش یافته" به این معنی نیست که کاربر هیچ حرکتی نمی خواهد. به جای موارد فوق، می‌توانید انیمیشن ظریف‌تری را انتخاب کنید، اما انیمیشنی که هنوز رابطه بین عناصر و جریان داده‌ها را بیان می‌کند.

تغییر انتقال بسته به نوع پیمایش

گاهی اوقات یک پیمایش از یک نوع خاص از صفحه به صفحه دیگر باید دارای یک انتقال خاص باشد. یا، ناوبری "بازگشت" باید با ناوبری "به جلو" متفاوت باشد.

انتقال های مختلف هنگام بازگشت به عقب. حداقل نسخه ی نمایشی . منبع .

بهترین راه برای رسیدگی به این موارد، تنظیم نام کلاس در <html> است که به عنوان عنصر document نیز شناخته می شود:

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 استفاده می‌کند، قولی که پس از رسیدن انتقال به حالت پایانی خود برطرف می‌شود. سایر خصوصیات این شی در مرجع API پوشش داده شده است.

اکنون می توانید از نام کلاس در 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;
}

و بس!

انتقال ویدیو، کندتر. حداقل نسخه ی نمایشی منبع .

اکنون ویدیو در طول انتقال پخش می شود.

متحرک سازی با جاوا اسکریپت

تا کنون، تمام انتقال ها با استفاده از 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 استفاده می کند، وعده ای که پس از ایجاد موفقیت آمیز شبه عناصر انتقال حل می شود. سایر خصوصیات این شی در مرجع API پوشش داده شده است.

انتقال به عنوان یک افزایش

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 برسد، رد می شود، اما این بدان معنا نیست که view تغییر نکرده است. ممکن است 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 جایگزین کنید.

پلی پر نیست، اما…

من فکر نمی کنم این ویژگی را بتوان به هیچ وجه مفید پر کرد، اما خوشحالم که ثابت شد اشتباه می کنم!

با این حال، این تابع کمکی کارها را در مرورگرهایی که از انتقال view پشتیبانی نمی‌کنند بسیار آسان‌تر می‌کند:

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

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

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

  const transition = document.startViewTransition(updateDOM);

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

  return transition;
}

و می توان از آن به صورت زیر استفاده کرد:

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

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

  // …
}

در مرورگرهایی که از View Transitions پشتیبانی نمی‌کنند، updateDOM همچنان فراخوانی می‌شود، اما انتقال متحرک وجود نخواهد داشت.

همچنین می‌توانید تعدادی classNames برای افزودن به <html> در طول انتقال ارائه دهید، که تغییر انتقال را بسته به نوع پیمایش آسان‌تر می‌کند.

همچنین اگر انیمیشنی نمی‌خواهید، حتی در مرورگرهایی که از View Transitions پشتیبانی می‌کنند، می‌توانید به skipTransition نیز true بفرستید. اگر سایت شما ترجیحات کاربر برای غیرفعال کردن انتقال ها را داشته باشد، مفید است.

کار با فریمورک ها

اگر با کتابخانه یا چارچوبی کار می‌کنید که تغییرات DOM را انتزاعی می‌کند، بخش مشکل این است که بدانید چه زمانی تغییر DOM کامل شده است. در اینجا مجموعه ای از مثال ها با استفاده از کمک کننده بالا در چارچوب های مختلف آورده شده است.

  • React— کلید در اینجا flushSync است که مجموعه ای از تغییرات حالت را به صورت همزمان اعمال می کند. بله، یک هشدار بزرگ در مورد استفاده از آن API وجود دارد، اما Dan Abramov به من اطمینان می دهد که در این مورد مناسب است. طبق معمول کدهای React و async، هنگام استفاده از وعده‌های مختلف بازگشتی توسط startViewTransition ، مراقب باشید که کد شما با وضعیت صحیح اجرا شود.
  • Vue.js — کلید اینجا nextTick است که پس از به‌روزرسانی DOM تکمیل می‌شود.
  • Svelte - بسیار شبیه به Vue است، اما روش انتظار برای تغییر بعدی tick است.
  • روشن - کلید در اینجا وعده this.updateComplete درون کامپوننت ها است که پس از به روز رسانی DOM محقق می شود.
  • Angular — کلید اینجا applicationRef.tick است که تغییرات در انتظار DOM را پاک می کند. از نسخه 17 Angular می‌توانید withViewTransitions که با @angular/router ارائه می‌شود استفاده کنید .

مرجع API

const viewTransition = document.startViewTransition(updateCallback)

یک ViewTransition جدید را شروع کنید.

updateCallback زمانی فراخوانی می شود که وضعیت فعلی سند ضبط شود.

سپس، زمانی که وعده داده شده توسط updateCallback محقق شد، انتقال در فریم بعدی آغاز می شود. اگر وعده بازگشتی توسط updateCallback رد شود، انتقال رها می شود.

اعضای نمونه ViewTransition :

viewTransition.updateCallbackDone

قولی که با تحقق وعده‌ای که updateCallback برگردانده می‌شود، عمل می‌کند، یا زمانی که آن را رد می‌کند، رد می‌شود.

View Transition API یک تغییر DOM را بسته بندی می کند و یک انتقال ایجاد می کند. با این حال، گاهی اوقات به موفقیت/شکست انیمیشن انتقال اهمیتی نمی دهید، فقط می خواهید بدانید که آیا تغییر DOM چه زمانی اتفاق می افتد یا خیر. updateCallbackDone برای آن مورد استفاده است.

viewTransition.ready

وعده ای که پس از ایجاد شبه عناصر برای انتقال محقق می شود و انیمیشن در شرف شروع است.

اگر انتقال نتواند شروع شود، رد می کند. این می تواند به دلیل پیکربندی نادرست باشد، مانند تکراری بودن view-transition-name ، یا اگر updateCallback یک وعده رد شده را برمی گرداند.

این برای متحرک سازی شبه عناصر انتقال با جاوا اسکریپت مفید است.

viewTransition.finished

وعده ای که زمانی محقق می شود که حالت پایان به طور کامل برای کاربر قابل مشاهده و تعاملی باشد.

فقط در صورتی رد می کند که updateCallback یک وعده رد شده را برگرداند، زیرا این نشان می دهد که حالت پایان ایجاد نشده است.

در غیر این صورت، اگر انتقالی شروع نشود، یا در طول انتقال از آن صرفنظر شود، همچنان به حالت پایان رسیده است، بنابراین finished انجام می‌شود.

viewTransition.skipTransition()

بخش انیمیشن انتقال را رد کنید.

این از فراخوانی updateCallback صرفنظر نمی کند، زیرا تغییر DOM جدا از انتقال است.

مرجع سبک پیش‌فرض و انتقال

::view-transition
شبه عنصر ریشه که viewport را پر می کند و شامل هر ::view-transition-group است.
::view-transition-group

کاملاً قرار گرفته است.

width و height بین حالت های «قبل» و «بعد» تغییر می دهد.

انتقال بین "قبل" و "پس از" فضای دید در چهار transform .

::view-transition-image-pair

کاملاً برای پر کردن گروه قرار گرفته است.

دارای isolation: isolate برای محدود کردن تأثیر حالت ترکیبی plus-lighter بر روی نماهای قدیمی و جدید، ایزوله کنید.

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

کاملاً در سمت چپ بالای لفاف قرار گرفته است.

100٪ عرض گروه را پر می کند، اما دارای ارتفاع خودکار است، بنابراین به جای پر کردن گروه، نسبت تصویر خود را حفظ می کند.

دارای mix-blend-mode: plus-lighter برای ایجاد یک متقاطع محو واقعی.

نمای قدیمی از opacity: 1 به opacity: 0 تغییر می کند. نمای جدید از opacity: 0 به opacity: 1 تغییر می کند.

بازخورد

بازخورد برنامه‌نویس در این مرحله بسیار مهم است، بنابراین لطفاً مشکلات را با پیشنهادات و سؤالات در GitHub ارسال کنید .