animation-timing-function задаёт кривую плавности анимации - по умолчанию ease, как и у transition. Но у анимации есть возможность, которой нет у transition: указать СВОЮ кривую для каждого отрезка внутри @keyframes. Работает это не так, как кажется на первый взгляд - кривая, заданная НА каком-то проценте, управляет отрезком ПОСЛЕ него, а не до него.
Попробуй прямо сейчас: мячик падает вниз, ускоряясь (animation-timing-function: ease-in - задан на элементе, управляет первым отрезком 0-50%), а потом отскакивает вверх, замедляясь к концу (animation-timing-function: ease-out - задан ПРЯМО на keyframe 50%, управляет ВТОРЫМ отрезком, 50-100%). Убери "animation-timing-function: ease-out;" из строки 50% - мячик начнёт подниматься вверх так же, как падал (снова ease-in, потому что больше некому переопределить отрезок) - естественного отскока не получится, мячик будто падает дважды подряд.
Кривая на весь цикл анимации
Самый простой способ - указать animation-timing-function прямо на элементе, как и у transition из урока про кривые плавности: те же ease/linear/ease-in/ease-out/ease-in-out, steps() и cubic-bezier() работают одинаково. По умолчанию, если ничего не указать, действует ease. Эта кривая применяется К КАЖДОМУ ЦИКЛУ анимации целиком, если только внутри @keyframes не задано что-то своё для конкретного отрезка (об этом - в следующем разделе).
.spinner {
animation-name: spin;
animation-duration: 2s;
animation-timing-function: linear;
} - вся анимация spin крутится с одной и той же равномерной скоростью.
Забыть про эту строку - не ошибка, а просто другое поведение:
.spinner {
animation-name: spin;
animation-duration: 2s;
} - animation-timing-function не указан, применяется значение по умолчанию ease, а не linear - вращение будет слегка неравномерным, с ускорением и замедлением.
Своя кривая для каждого отрезка внутри @keyframes
У анимации, в отличие от transition, есть возможность задать РАЗНУЮ кривую плавности для разных отрезков одного цикла - animation-timing-function можно написать прямо внутри блока конкретного процента в @keyframes. Здесь и кроется неочевидный момент, подтверждённый собственной проверкой: кривая, указанная НА каком-то проценте (например 50%), управляет отрезком ПОСЛЕ него - от 50% до следующего keyframe, а НЕ отрезком, который к 50% ведёт. Если на конкретном проценте ничего не указано, для его отрезка берётся значение с самого элемента (как в первом разделе). А кривая, указанная на ПОСЛЕДНЕМ keyframe (100% или to), не имеет никакого эффекта - применять её больше не к чему, следующего отрезка не существует.
@keyframes drop {
0% { top: 0; }
50% { top: 150px; animation-timing-function: ease-out; }
100% { top: 0; }
}
.ball { animation: drop 1.6s ease-in infinite; } - падение (0-50%) идёт по ease-in с элемента, а подъём (50-100%) - по ease-out с самого keyframe 50%.
Частая путаница - думать, что кривая на 50% относится к пути ДО него:
@keyframes drop {
0% { top: 0; animation-timing-function: ease-out; }
50% { top: 150px; }
100% { top: 0; }
} - кривая на 0% на самом деле управляет отрезком 0-50% (это верно), но многие ожидают, что писать плавность нужно НА том проценте, к которому она "относится по смыслу" - и путают, куда писать при более сложных цепочках из 3+ процентов.
Частые ошибки новичков
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Думать, что плавность на keyframe X относится к отрезку ДО X | На самом деле она управляет отрезком ПОСЛЕ X, до следующего keyframe | Писать плавность на том проценте, С КОТОРОГО начинается нужный отрезок, а не на том, к которому он ведёт |
| Указать animation-timing-function на последнем keyframe (100% или to) | Никакого эффекта - применять кривую больше не к чему, следующего отрезка нет | Указывать нужную кривую на ПРЕДПОСЛЕДНЕМ значимом проценте или на элементе |
| Не указать animation-timing-function нигде | Используется ease по умолчанию - может быть не тем, что ожидалось для чёткой покадровой анимации | Явно указывать нужную кривую, если важна предсказуемость |
| Пытаться плавно изменить скорость/плавность анимации через transition при :hover | animation-duration и animation-timing-function не анимируются - значение переключается мгновенно, без перехода | Для плавного изменения скорости уже запущенной анимации нужен JavaScript, а не CSS transition |
Шпаргалка
| Запись | Что означает |
|---|---|
| animation-timing-function по умолчанию | ease - как и у transition |
| animation-timing-function: linear; (на элементе) | Вся анимация играет с равномерной скоростью |
| 50% { animation-timing-function: X; } | X управляет отрезком ОТ 50% до следующего keyframe |
| 100% { animation-timing-function: X; } | Не имеет эффекта - следующего отрезка нет |
| Кривая не указана на конкретном проценте | Берётся значение с элемента (fallback) |
| transition: animation-duration 1s; | Не работает - значение меняется мгновенно, не анимируется |
Коротко: отвечаем на вопросы
Я написал animation-timing-function прямо на нужном keyframe, а плавность как будто применяется к предыдущему отрезку. Почему?
Это известная особенность спецификации, которую разработчики называют "confusing API" - кривая, указанная НА каком-то проценте, управляет отрезком ПОСЛЕ него, до следующего keyframe, а не отрезком, который к этому проценту ведёт. Если нужна своя кривая для самого первого отрезка (от 0%), её нужно указывать на элементе или на самом 0% keyframe - а не ждать, что она сработает "задним числом" для пути, который уже был пройден.
Можно ли плавно ускорить или замедлить уже запущенную CSS-анимацию, например через :hover и transition?
Нет - реальный ответ с Хабра подтверждает, что animation-duration не анимируется. То же самое верно и для animation-timing-function: значение меняется мгновенно, без интерполяции, даже если на свойство навешан transition. Собственная проверка показала, что смена длительности с 4s на 1s происходит без единого промежуточного значения. Для настоящего плавного изменения скорости анимации нужен JavaScript, управляющий, например, playbackRate у Web Animations API.