Анимации и переходы
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-техника
Плавное перемещение элемента:
- First — запомнить позицию до
- Last — изменить DOM
- Invert — сдвинуть обратно
- 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