У меня есть два элемента с анимацией. Анимация первых элементов длится 1 секунду и имеет кривую замедления cubic-bezier(0.5, 0, 0.5, 1)
(замедление). Анимация второго элемента начинается 0.5s
после первого, длится 0.5s
и имеет кривую замедления cubic-bezier(0, 0, 0.5, 1)
(только замедление). Я думал, что это позволит их анимации совпадать, поскольку обе кривые заканчиваются одинаковым замедлением, но первый элемент в середине движения движется немного быстрее, чем второй. Как сделать так, чтобы анимация совпадала? И если уж на то пошло, как мне совместить любую кривую замедления с кривой плавности входа/выхода в аналогичной ситуации?
Пример проблемы:
const first = document.getElementById("first");
const second = document.getElementById("second");
function animateit() {
first.classList.add("animated");
setTimeout(() => second.classList.add('animated'), 500)
}
function reset() {
first.classList.remove('animated');
second.classList.remove('animated');
}
* {
margin: 5px;
}
div {
width: 50px;
height: 50px;
background: black;
}
#first {
transition: 1s cubic-bezier(0.5, 0, 0.5, 1);
}
#second {
transition: 0.5s cubic-bezier(0, 0, 0.5, 1);
transform: translateX(100px);
}
.animated {
transform: translateX(200px) !important;
}
<button onclick='animateit()'>animate</button>
<button onclick='reset()'>reset</button>
<div id='first'></div>
<div id='second'></div>
Я попробовал изменить ослабление второго элемента и очень близко подошел к cubic-bezier(0.3, 0.6, 0.5, 1)
, но оно все равно не полностью совпадает.
Кривые Безье нелинейны, поэтому, если вы возьмете две кривые, одна из которых начинается в A и заканчивается в B, а другая начинается с (A+B)/2 и заканчивается в B, обе с правилами облегчения выхода. , то обе кривые будут иметь максимальную скорость в средней точке, но кривые не будут выглядеть одинаково и не будут совпадать по скорости:
0.5, 0, 0.5, 1 выглядит так:
Итак, если нам нужна кривая, которая захватывает только «вторую половину этой кривой», то есть этот раздел:
тогда нам нужно разделить кубическую кривую в этой средней точке. В результате мы получим две кривые, первая из которых нас не волнует, а вторая начинается с (0,5,0,5), имеет первую контрольную точку (0,625, 0,75), вторую контрольную точку (0,75). , 1) и его конечная точка в (1,1):
Это неправильное начало для целей CSS, но кривые Безье инвариантны к масштабированию (пока мы масштабируем x и y на одинаковую величину), поэтому мы можем просто масштабировать все координаты так, чтобы получить кривую, которая начинается с (0 ,0), с первой контрольной точкой в (0,25, 0,5), второй контрольной точкой в (0,5, 1), а затем заканчивающейся в (1,1), точно так, как того требует CSS:
#second {
transition: 0.5s cubic-bezier(0.25, 0.5, 0.5, 1); /* heck yeah! */
transform: translateX(100px);
}
Мы посчитали, вперед! Все, что нам нужно сделать, это обновить кубические значения, и все готово!
Вот только… не совсем? Если мы просто воспользуемся этими значениями (и вы уже сталкивались с этим), то мы увидим, что все работает только «иногда», но почти всегда тайминги сбиваются и все вообще не совпадает:
const first = document.getElementById("first");
const second = document.getElementById("second");
function animateit() {
first.classList.add("animated");
setTimeout(() => second.classList.add('animated'), 500)
}
function reset() {
first.classList.remove('animated');
second.classList.remove('animated');
}
* {
margin: 5px;
}
div {
width: 50px;
height: 50px;
background: black;
}
#first {
transition: 1s cubic-bezier(0.5, 0, 0.5, 1);
}
#second {
transition: 0.5s cubic-bezier(0.25, 0.5, 0.5, 1);
transform: translateX(100px);
}
.animated {
transform: translateX(200px) !important;
}
<button onclick='animateit()'>animate</button>
<button onclick='reset()'>reset</button>
<div id='first'></div>
<div id='second'></div>
Что происходит?
Как оказалось: JavaScript работает, и мы не хотим, чтобы он: setTimeout
(и setInterval
) крайне ненадежны с точки зрения того, когда они сработают, потому что единственная гарантия, которую вы получите, это то, что они Я выстрелю «как минимум через X миллисекунд»… но не более того. Если setTimeout
на 500 мс занимает 501 мс, это вполне соответствует спецификации. Если это занимает 510 мс, это тоже в пределах спецификации. И если на стрельбу уходит час, это все равно в пределах нормы. Мы не можем использовать таймеры JS, если нам нужно, чтобы события запускались в определенное время. В JS просто нет ничего, что можно было бы сделать.
(по крайней мере, без некоторых суперкреативных лайфхаков)
К счастью, CSS предлагает нам очень удобный инструмент, который позволяет нам полностью обойти эту проблему: вместо запуска таймеров мы можем просто использовать CSS-анимацию и воспользоваться правилом задержки анимации. Мы можем просто настроить CSS так, чтобы у нас была одинаковая анимация для обоих блоков, начиная с «их начальной позиции» (для которой мы можем использовать переменную CSS: 0 пикселей для первого блока и половину расстояния до конца для второго блока). ) до «конечной позиции» (что составляет 200 пикселей для обоих, но я собираюсь сделать это 400 пикселей, чтобы эффект был еще более очевидным):
#group {
--base: 0px;
--end: 400px;
.box {
transform: translateX(var(--base));
}
.second {
--base: calc(var(--end) / 2 );
}
}
Затем мы указываем первому блоку анимацию в течение 1 секунды, начиная немедленно, и мы приказываем второму блоку анимировать полсекунды, подождав полсекунды:
.first {
animation-name: move;
animation-duration: 1s;
animation-timing-function: cubic-bezier(.5, 0, .5, 1);
}
.second {
animation-name: move;
animation-delay: 0.5s;
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(.25, .5, .5, 1);
}
И если мы это сделаем, то теперь все пойдет идеально, точно так, как и предполагалось:
animate.addEventListener(`click`, () => {
group.classList.add(`animate`);
setTimeout(() => group.classList.remove(`animate`), 3000);
});
* {
margin: 5px;
}
#group {
--base: 0px;
--end: 400px;
.box {
width: 50px;
height: 50px;
background: black;
transform: translateX(var(--base));
&.second {
--base: calc(var(--end) / 2);
}
}
&.animate {
.box {
animation-name: move;
animation-fill-mode: forwards;
}
.first {
animation-duration: 1s;
animation-timing-function: cubic-bezier(.5, 0, .5, 1);
}
.second {
animation-delay: 0.5s;
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(.25, .5, .5, 1);
}
}
}
@keyframes move {
from {
transform: translateX(var(--base));
}
to {
transform: translateX(var(--end));
}
}
<button id = "animate">animate</button>
<div id = "group">
<div class='first box'></div>
<div class='second box'></div>
</div>
Большое спасибо за ответ и за то, что сообщили мне о проблеме синхронизации JS. Я понятия не имел, что это настолько неточно! Кстати, после того, как вы отредактировали ответ, заметил небольшую опечатку в ссылке по поводу разделения кривых. Там была добавлена лишняя буква «Т», поэтому я пошел дальше и исправил ее.
Stackoverflow не позволял мне сохранять изменения, если я не добавлял более 6 символов, поэтому мне также пришлось добавить описания к изображениям xD
Спасибо за обновление. Я написал ответ, который показывает, как мы можем определить, какая кривая нам нужна, и как мы можем сделать это с помощью только CSS, потому что таймеры JS совершенно ненадежны. Единственная гарантия, которая у вас когда-либо будет, это то, что они будут ждать «по крайней мере» столько мс, сколько вы им сказали. Не то чтобы они сработали точно или даже близко к указанному вами количеству мс. В отличие от правил синхронизации анимации CSS, которые представляют собой точные значения.