По-настоящему красивые переходы средствами браузера

Никита Дубко, «Веб-стандарты»

По-настоящему
красивые
переходы
средствами
браузера

Никита Дубко, «Веб-стандарты»

Никита Дубко

Веб-стандарты

CSS Only

Без библиотек

Часть 1.
Анимации

Stacking Cards demo

gzip_size https://static.tildacdn.com/js/tilda-animation-2.0.min.js
Plain: 34298
Gzipped: 6749

CDN != Cache

Intersection
Observer API

MDN
const observer = new IntersectionObserver(
    callback,
    {
        root: document.querySelector('#scrollArea'),
        rootMargin: '0px',
        threshold: [1.0],
    }
);
const callback = (entries, observer) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            if (entry.intersectionRatio >= 0.75) {
                // TODO
            }
        }
    });
};

Что такое анимация?

animation: hadouken 2s linear infinite;
animation-name: hadouken;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;

2s = 100%

animation = F(t)

function easeInOut(t) {
    if (t <= 0.5) {
        return 2 * t * t;
    }
    t -= 0.5;
    return 2 * t * (1 - t) + 0.5;
}

А если привязаться к скроллу?

Progress
@keyframes grow-progress {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
}

.target {
    transform-origin: 0 50%;
    animation: grow-progress auto linear;
    animation-timeline: scroll();
}
animation-timeline: scroll(<scroller> <axis>)

<scroller>

  • nearest
  • root
  • self

<axis>

  • inline ͢
  • block↓
  • x ͢
  • y↓

А если привязаться к чему угодно?

@keyframes animate-it { … }

.scroller {
    scroll-timeline-name: --my-scroller;
    scroll-timeline-axis: inline;
    /* scroll-timeline: --my-scroller inline; */
}

.target {
    animation: animate-it linear;
    animation-timeline: --my-scroller;
}
Animate elements on scroll with Scroll-driven animations

Web Animations API?

const $progressbar = document.querySelector('#progress');

$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
    {
        transform: ['scaleX(0)', 'scaleX(1)'],
    },
    {
        fill: 'forwards',
        timeline: new ScrollTimeline({
            source: document.documentElement,
        }),
    }
);
animation-timeline: view(<axis> <view-timeline-inset>)

<axis>

  • block ͢
  • inline↓
  • x ͢
  • y↓

<view-timeline-inset>

  • %
  • auto
.revealing-image {
    view-timeline-name: --revealing-image;
    view-timeline-axis: block;

    animation: auto linear reveal both;
    animation-timeline: --revealing-image;
    animation-range: entry 25% cover 50%;
}

@keyframes reveal {
    from { opacity: 0; }
    to { opacity: 1; }
}
animation-range: entry 0% entry 100%;

animation-range

Range and Animation Progress Visualizer
$el.animate(
    {
        transform: ['scaleX(0)', 'scaleX(1)'],
    },
    {
        timeline: new ViewTimeline({
            subject: document.getElementById('subject'),
        }),
        rangeStart: 'entry 25%',
        rangeEnd: 'cover 50%',
    }
);

x.com/jh3yy

.icon {
    animation: icon-scale, icon-scale;
    animation-direction: normal, reverse;
    animation-timeline: view(inline);
    animation-range: entry, exit;
}

@​keyframes icon-scale { 0% { scale: 0; } }
CSS property: animation-timeline
scroll-timeline

Часть 2.
Переходы

TransitionGroup
Animation transitions and triggers

Cложно 😔

Много весит 🐳

Инженерное мышление

opacity

<meta http-equiv="Page-Enter" content="blendTrans(Duration=1.0)">
<meta http-equiv="Page-Exit" content="blendTrans(Duration=1.0)">
<meta http-equiv="Site-Enter" content="blendTrans(Duration=1.0)">
<meta http-equiv="Site-Exit" content="blendTrans(Duration=1.0)">
Microsoft: FrontPage FAQ

🪄 View Transitions API


document.startViewTransition(() => {

    updateTheDOMSomehow(data);

});
// 1. Делаем скриншот
document.startViewTransition(() => {

    updateTheDOMSomehow(data);

});
// 1. Делаем скриншот
document.startViewTransition(() => {
    // 2. Состояние DOM сохранено
    updateTheDOMSomehow(data);

});
// 1. Делаем скриншот
document.startViewTransition(() => {
    // 2. Состояние DOM сохранено
    updateTheDOMSomehow(data);
    // 3. Запускаем магию
});
::view-transition
└─ ::view-transition-group(root)
    └─ ::view-transition-image-pair(root)
        ├─ ::view-transition-old(root)
        └─ ::view-transition-new(root)
::view-transition-old(root)
    opacity: 1 ➞ 0

::view-transition-new(root)
    opacity: 0 ➞ 1
::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 5s;
}
.card {
    view-transition-name: card;
}
::view-transition
└─ ::view-transition-group(card)
    └─ ::view-transition-image-pair(card)
        ├─ ::view-transition-old(card)
        └─ ::view-transition-new(card)
Smooth and simple transitions with the View Transitions API
Chrome DevTools — спрятанные полезности / Никита Дубко
::view-transition
└─ ::view-transition-group(sidebar)
    └─ ::view-transition-image-pair(sidebar)
        └─ ::view-transition-new(sidebar)
/* Появление */
::view-transition-new(sidebar):only-child {
    animation: 300ms linear both slide-from-right;
}

/* Пропадание */
::view-transition-old(sidebar):only-child {
    animation: 300ms linear both slide-to-right;
}

Инженерное мышление

Promise.race

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

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

    await Promise.race([document.fonts.ready, wait(100)]);
});

☝️ Только SPA

export async function getPageContent(url) {
    // Не используйте в продакшене!
    const response = await fetch(url);
    const text = await response.text();

    return /<body[^>]*>([\w\W]*)<\/body>/.exec(text)[1];
}
Demo

📱 А для мобилок?

@media

Доступность?

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

console.log(transition);

transition.finished

transition.ready

View Transitions API (single-document)
WebKit Standards Positions
Mozilla Standards Positions
function startViewTransition(callback) {
    if (!document.startViewTransition) {
        callback();
        return;
    }

    document.startViewTransition(callback);
}
View Transitions Break Incremental Rendering
<link rel="prefetch" href="/another-page">

<script type="speculationrules">
    {
        "prerender": [
            {
                "source": "list",
                "urls": ["page-1.html", "page-2.html"]
            }
        ]
    }
</script>
        
GoogleChromeLabs/quicklink

x.com/bramus

noamr / velvette

Tutorial - Extend with View Transitions
Cross Document View Transitions

Часть 3.
Практика

🌌 Демо

@keyframes header {
    from {
        filter: hue-rotate(0turn);
    }
    to {
        filter: hue-rotate(1turn);
    }
}

header::before {
    animation: header auto linear;
    animation-timeline: scroll();
}

🌌 Версия со scroll()

.card {
    transform-origin: 50% 0;
    animation: card ease both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
}

@keyframes card {
    0% { translate: -100%; filter: blur(10px); opacity: 0; }
    100% { translate: 0; filter: blur(0); opacity: 1; }
}

🌌 Версия с view()

onLinkNavigate(async ({ fromPath, toPath }) => {
    const content = await getPageContent(toPath);

    const avatar = getLink(toPath)
        .closest(".card")
        .querySelector("img");
    avatar.style.viewTransitionName = "avatar-img";

    transitionHelper({
        updateDOM() {
            document.body.innerHTML = content;
        },
    });
});
.talk img {
    view-transition-name: avatar-img;
}

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

🌌 View Transitions API

😉 Пора меняться!

Красота спасёт мир!

Слайды
mefody.dev/talks/pretty-transitions/

Подписаться
@dark_mefody
t.me/mefody_dev QR-код со ссылкой на доклад