Собесов

Frontend Яндекса — машина Голдберга на CSS (анимации без JS)

Кейсы и метрикиCSS-анимацииСложнаяSenior

Условие

Дана готовая страница с «машиной Голдберга». HTML и базовые стили уже есть. JavaScript использовать нельзя. Вёрстку трогать тоже нельзя — только дописать CSS.

Запуск анимации происходит через чекбокс #start и селекторы состояния (:checked). Дальше — через CSS-анимации и задержки. Цепочка должна устойчиво работать с повторным переключением состояния (запуск/остановка).

Видео-эталон

Точные координаты и тайминги не выводятся — нужно по видео добиться максимально похожего поведения.

Примечание

При запуске скрипт должен оставаться неподвижным, а решение должно быть устойчивым с повторному переключению состояний.

Решение

Подход

Это задача на CSS checkbox hack + цепочка анимаций с разной задержкой animation-delay.

1. Базовый «триггер»

<input type="checkbox" id="start" />
<label for="start">Старт</label>
<div class="scene">…</div>
#start { display: none; }
#start:checked ~ .scene .ball {
  animation: roll 2s linear forwards;
}
@keyframes roll {
  from { transform: translateX(0); }
  to   { transform: translateX(300px); }
}

2. Цепочка событий

Каждый элемент машины Голдберга стартует с задержкой относительно момента :checked:

#start:checked ~ .scene .ball {
  animation: roll 1.2s linear forwards;
}
#start:checked ~ .scene .domino-1 {
  animation: fall 0.4s ease-in 1.2s forwards;
}
#start:checked ~ .scene .domino-2 {
  animation: fall 0.4s ease-in 1.6s forwards;
}
#start:checked ~ .scene .lever {
  animation: tilt 0.8s ease-out 2.0s forwards;
}
/* … */

3. Сброс при выключении чекбокса

Без forwards анимация после animation-end сбрасывается в исходное состояние. Если forwards стоит — мы остаёмся в финальном состоянии. При снятии галочки селектор #start:checked ~ ... перестанет совпадать → CSS-движок перевычислит — анимаций нет → элементы вернутся в исходные transform: ....

Чтобы не было «прыжка», добавляем переход возврата:

.ball {
  transform: translateX(0);
  transition: transform 0.5s linear;
}
#start:checked ~ .scene .ball {
  /* Анимация перехватывает transition */
  animation: roll 1.2s linear forwards;
}

Когда :checked снимается — animation пропадает, transition плавно возвращает в translateX(0).

Альтернатива: анимация-возврат через @keyframes для не-:checked варианта.

4. Устойчивость к повторным переключениям

Главное правило: исходное состояние должно быть в обычных transform: ... (или базовых стилях). Не оставлять стейт «в анимации» без forwards или transition для возврата.

Полный пример

<input type="checkbox" id="start" />
<label for="start" class="btn">Запустить машину</label>
<div class="scene">
  <div class="ball"></div>
  <div class="domino domino-1"></div>
  <div class="domino domino-2"></div>
  <div class="lever"></div>
  <div class="bucket"></div>
</div>
.scene { position: relative; height: 300px; }
.ball, .domino, .lever, .bucket { position: absolute; transition: all 0.5s linear; }
 
.ball     { left: 0;   top: 0;   width: 30px; height: 30px; background: red; border-radius: 50%; }
.domino-1 { left: 320px; top: 280px; width: 10px; height: 80px; background: brown; transform-origin: bottom; }
.domino-2 { left: 360px; top: 280px; width: 10px; height: 80px; background: brown; transform-origin: bottom; }
.lever    { left: 400px; top: 250px; width: 100px; height: 10px; background: gray; transform-origin: left; }
.bucket   { left: 520px; top: 200px; width: 50px; height: 50px; background: orange; }
 
@keyframes roll        { to { transform: translateX(300px); } }
@keyframes fall        { to { transform: rotate(75deg);     } }
@keyframes tilt        { to { transform: rotate(45deg);     } }
@keyframes drop        { to { transform: translateY(60px);  } }
 
#start:checked ~ .scene .ball     { animation: roll 1.2s linear         forwards; }
#start:checked ~ .scene .domino-1 { animation: fall 0.4s ease-in 1.2s   forwards; }
#start:checked ~ .scene .domino-2 { animation: fall 0.4s ease-in 1.6s   forwards; }
#start:checked ~ .scene .lever    { animation: tilt 0.8s ease-out 2.0s  forwards; }
#start:checked ~ .scene .bucket   { animation: drop 0.6s ease-in   2.8s forwards; }

Подводные камни

  1. animation-delay ≠ синхронизация с реальными событиями. Если ball едет 1.2s, домино должна стартовать в 1.2s, не в 1.0s. Точные тайминги — вручную из видео.
  2. forwards обязателен. Без него анимация откатывается → следующая анимация начнётся уже не из конечного состояния.
  3. Невозможно «остановить» анимацию посередине через CSS. animation-play-state: paused через :checked работает, но при сбросе :checked анимация не сбрасывает прогресс — а полностью пропадает (откатываемся к началу). Это даёт скачок. Для плавности — transition на возврат.
  4. ~ против +. #start:checked ~ .scene .ball — общий sibling-селектор: .scene должна быть ниже чекбокса в DOM. Если выше — селектор не сработает, нужен другой паттерн.
  5. transition побеждает animation? Не всегда. Если оба заданы, animation имеет приоритет, но при удалении animation transition сработает на возврат.
  6. transform-origin для домино. Без него домино «закрутится» вокруг центра, а не упадёт.
  7. Производительность. Анимировать transform и opacity (не top/left/width) — они GPU-ускоренные.

Эталонный ответ

CSS checkbox hack: #start:checked ~ .scene .X { animation: ... 0.X s X.Xs forwards; } для каждого элемента сцены, с правильно подобранными animation-delay. Возврат — через transition на свойствах transform/opacity.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти