Урок 09

Анимации и переходы

CSS-переходы

.button {
    transition: background 0.3s ease;
}

.button:hover {
    background: red;
}

JavaScript меняет класс — CSS анимирует:

button.addEventListener('click', () => {
    button.classList.toggle('active');
});

Событие transitionend

el.addEventListener('transitionend', (event) => {
    console.log('переход завершён', event.propertyName);
});

Проверка:

el.addEventListener('transitionend', (event) => {
    if (event.propertyName === 'opacity') {
        el.remove();
    }
});

Событие animationend

el.addEventListener('animationend', () => {
    console.log('анимация завершена');
});

Также: animationstart, animationiteration.

requestAnimationFrame

Для JS-анимаций:

let x = 0;

function animate() {
    x += 2;
    el.style.transform = `translateX(${x}px)`;

    if (x < 500) {
        requestAnimationFrame(animate);
    }
}

requestAnimationFrame(animate);

Синхронизировано с частотой монитора (~60 fps).

Плавная анимация с delta time

let last = performance.now();
let x = 0;

function animate(now) {
    const dt = (now - last) / 1000;
    last = now;

    x += 100 * dt; // 100 пикселей в секунду
    el.style.transform = `translateX(${x}px)`;

    if (x < 500) requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Скорость одинаковая на 60 и 120 Hz.

Web Animations API

el.animate([
    { transform: 'translateX(0)', opacity: 1 },
    { transform: 'translateX(500px)', opacity: 0 },
], {
    duration: 1000,
    easing: 'ease-in-out',
    fill: 'forwards',
});

Управление анимацией

const animation = el.animate([...], { duration: 1000 });

animation.pause();
animation.play();
animation.cancel();
animation.reverse();

animation.onfinish = () => console.log('готово');

Промис из анимации

function animate(el, keyframes, options) {
    const anim = el.animate(keyframes, options);
    return anim.finished;
}

await animate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 300 });

Плавное появление

async function fadeIn(el) {
    el.style.opacity = '0';
    el.style.display = 'block';

    await el.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 300,
        fill: 'forwards',
    }).finished;
}

async function fadeOut(el) {
    await el.animate([{ opacity: 1 }, { opacity: 0 }], {
        duration: 300,
        fill: 'forwards',
    }).finished;

    el.style.display = 'none';
}

Слайд-меню

async function openMenu() {
    menu.style.display = 'block';

    await menu.animate([
        { transform: 'translateX(-100%)' },
        { transform: 'translateX(0)' },
    ], {
        duration: 300,
        easing: 'ease-out',
        fill: 'forwards',
    }).finished;
}

FLIP-техника

Плавное перемещение элемента:

  1. First — запомнить позицию до
  2. Last — изменить DOM
  3. Invert — сдвинуть обратно
  4. Play — анимировать
function moveWithAnimation(el, target) {
    const first = el.getBoundingClientRect();

    target.appendChild(el);

    const last = el.getBoundingClientRect();

    const dx = first.left - last.left;
    const dy = first.top - last.top;

    el.animate([
        { transform: `translate(${dx}px, ${dy}px)` },
        { transform: 'translate(0, 0)' },
    ], { duration: 300 });
}

Что анимировать

Анимируй transform и opacity — они не вызывают reflow:

// Плохо
el.style.left = `${x}px`;
el.style.width = '200px';

// Хорошо
el.style.transform = `translateX(${x}px)`;
el.style.transform = 'scale(1.5)';

will-change

Подсказка браузеру:

.element {
    will-change: transform;
}

Не злоупотребляй — может ухудшить производительность.

Проверка предпочтений

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
    animate(el);
}

Итоги

  • CSS-переходы + transitionend
  • requestAnimationFrame для JS-анимаций
  • Delta time для независимости от FPS
  • Web Animations API — удобный способ
  • FLIP для перемещений
  • Анимируй transform и opacity