Wolfenstein лише для CSS це невеликий проект, який я зробив кілька тижнів тому. Це був експеримент із 3D-перетвореннями та анімацією CSS.
Натхненний Демо FPS і ще Wolfenstein CodePen, я вирішив створити власну версію. Він приблизно заснований на Епізоді 1 – Поверх 9 оригінальної гри Wolfenstein 3D.
Редактор: ця гра навмисно вимагає швидкої реакції, щоб уникнути екрана Game Over.
Ось відео про проходження:
У двох словах, мій проект — це не що інше, як ретельно написана довга CSS-анімація. Плюс кілька випадків прапорець зламати.
:checked ~ div { animation-name: spin; }
Середовище складається з граней тривимірної сітки, а анімація здебільшого є звичайним тривимірним перекладом і обертанням. Нічого особливого.
Однак вирішити дві проблеми було особливо складно:
- Відтворюйте анімацію «стріляння зі зброї», коли гравець натискає на ворога.
- Коли швидкий бос отримав останній удар, увімкніть драматичне сповільнене відтворення.
На технічному рівні це означало:
- Повторне відтворення анімації, коли встановлено наступний прапорець.
- Уповільнення анімації, коли встановлено прапорець.
Насправді жодне з них не було належним чином вирішено в моєму проекті! Я або скористався обхідними шляхами, або просто відмовився.
З іншого боку, після деякого копання я зрештою знайшов ключ до обох проблем: зміна властивостей запуску анімації CSS. У цій статті ми докладніше розглянемо цю тему:
- Багато інтерактивних прикладів.
- Dissections: як кожен приклад працює (чи не працює)?
- За кадром: як браузери обробляють стани анімації?
Дозволь мені «кидай мої цеглини».
Проблема 1: Повторне відтворення анімації
Перший приклад: «ще один прапорець»
Моєю першою інтуїцією було «просто додайте ще один прапорець», що не працює:
Кожен прапорець працює окремо, але не обидва разом. Якщо один прапорець уже встановлено, інший більше не працює.
Ось як це працює (або «не працює»):
- Команда
animation-name
of<div>
isnone
за замовчуванням - Користувач натискає один прапорець,
animation-name
стаєspin
, і анімація починається спочатку. - Через деякий час користувач натискає інший прапорець. Нове правило CSS вступає в силу, але
animation-name
is як і ранішеspin
, що означає, що анімація не додається та не видаляється. Анімація просто продовжує відтворюватися, ніби нічого не сталося.
Другий приклад: «клонування анімації»
Одним з робочих підходів є клонування анімації:
#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2; }
Ось як це працює:
animation-name
isnone
спочатку.- Користувач натискає «Оберти!»,
animation-name
стаєspin1
. Анімаціяspin1
розпочато з самого початку, оскільки його щойно додали. - Користувач натискає «Оберни знову!»,
animation-name
стаєspin2
. Анімаціяspin2
розпочато з самого початку, оскільки його щойно додали.
Зауважте, що на кроці №3 spin1
видаляється через порядок правил CSS. Це не спрацює, якщо «Оберни знову!» перевіряється спочатку.
Третій приклад: «додавання тієї самої анімації»
Інший робочий підхід полягає в «додаванні тієї самої анімації»:
#spin1:checked ~ div { animation-name: spin; }
#spin2:checked ~ div { animation-name: spin, spin; }
Це схоже на попередній приклад. Ви насправді можете зрозуміти цю поведінку таким чином:
#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2, spin1; }
Зауважте, що коли напис "Spin again!" позначено, стара запущена анімація стає другою анімацією в новому списку, що може бути неінтуїтивним. Прямий наслідок: трюк не спрацює, якщо animation-fill-mode
is forwards
. Ось демо:
Якщо вам цікаво, чому це так, ось кілька підказок:
animation-fill-mode
isnone
за замовчуванням, що означає «Анімація не має жодного ефекту, якщо не відтворюється».animation-fill-mode: forwards;
означає «Після завершення відтворення анімації вона має залишатися на останньому ключовому кадрі назавжди».spin1
рішення завжди скасовуютьсяspin2
це тому, щоspin1
з’являється пізніше в списку.- Припустімо, що користувач натискає «Оберти!», чекає повного обертання, а потім натискає «Оберти знову!». Зараз.
spin1
вже закінчено, іspin2
тільки починається.
Обговорення
Емпіричне правило: ви не можете «перезапустити» наявну анімацію CSS. Натомість ви хочете додати та відтворити нову анімацію. Це може бути підтверджено специфікація W3C:
Після того, як анімація почалася, вона продовжується, доки не закінчиться або не буде видалено назву анімації.
Порівнюючи останні два приклади, я думаю, що на практиці «клонування анімації» часто має працювати краще, особливо коли доступний препроцесор CSS.
Проблема 2: Уповільнений рух
Хтось може подумати, що уповільнення анімації – це лише питання встановлення довшого animation-duration
:
div { animation-duration: 0.5s; }
#slowmo:checked ~ div { animation-duration: 1.5s; }
Дійсно, це працює:
… чи так?
За допомогою кількох налаштувань проблему буде легше побачити.
Так, анімація сповільнена. І ні, це не виглядає добре. Собака (майже) завжди «стрибає», коли ви перемикаєте прапорець. Крім того, здається, що собака стрибає у випадкове положення, а не в початкове. Як так?
Це було б легше зрозуміти, якби ми ввели два «елементи тіні»:
Обидва елементи тіні запускають однакову анімацію з різними animation-duration
. І на них не впливає прапорець.
Коли ви перемикаєте прапорець, елемент одразу перемикається між станами двох тіньових елементів.
Цитувати специфікація W3C:
Зміни значень властивостей анімації під час виконання анімації застосовуються так, ніби анімація мала ці значення з моменту її початку.
Це випливає з без громадянства дизайн, який дозволяє браузерам легко визначати значення анімації. Описано фактичний розрахунок тут та тут.
Чергова спроба
Одна з ідей полягає в тому, щоб призупинити поточну анімацію, а потім додати повільнішу анімацію, яка буде виконуватися звідти:
div {
animation-name: spin1;
animation-duration: 2s;
}
#slowmo:checked ~ div {
animation-name: spin1, spin2;
animation-duration: 2s, 5s;
animation-play-state: paused, running;
}
Так це працює:
… чи так?
Він сповільнюється, коли ви натискаєте «Slowmo!». Але якщо ви дочекаєтеся повного кола, ви побачите «стрибок». Насправді, він завжди переходить у положення, коли "Slowmo!" натиснуто.
Причина в тому, що у нас немає from
визначено ключовий кадр – і ми не повинні. Коли користувач натискає «Slowmo!», spin1
зупиняється на певній позиції, і spin2
починається точно з того самого положення. Ми просто не можемо передбачити цю позицію заздалегідь ... чи можемо?
Працююче рішення
Ми можемо! Використовуючи спеціальну властивість, ми можемо зафіксувати кут у першій анімації, а потім передати його до другої анімації:
div {
transform: rotate(var(--angle1));
animation-name: spin1;
animation-duration: 2s;
}
#slowmo:checked ~ div {
transform: rotate(var(--angle2));
animation-name: spin1, spin2;
animation-duration: 2s, 5s;
animation-play-state: paused, running;
}
@keyframes spin1 {
to {
--angle1: 360deg;
}
}
@keyframes spin2 {
from {
--angle2: var(--angle1);
}
to {
--angle2: calc(var(--angle1) + 360deg);
}
}
Примітка: @property
використовується в цьому прикладі, який є не підтримується всіма браузерами.
«Ідеальне» рішення
До попереднього рішення є застереження: «вихід із slowmo» не працює належним чином.
Ось краще рішення:
У цій версії можна плавно ввімкнути або вийти з повільного відтворення. Також не використовується експериментальна функція. То чи це ідеальне рішення? Так і ні.
Це рішення працює як «перемикання» «передач»:
- Шестерні: є дві
<div>
с. Один є батьком іншого. Обидва маютьspin
анімація, але з іншоюanimation-duration
. Кінцевим станом елемента є накопичення обох анімацій. - Перемикання: на початку лише одне
<div>
працює анімація. Інший призупинено. Коли прапорець перемикається, обидві анімації міняються місцями.
Хоча мені дуже подобається результат, є одна проблема: це приємний експлойт spin
анімація, яка взагалі не працює для інших типів анімації.
Практичне рішення (з JS)
Для загальних анімацій можна досягти функції уповільненого руху за допомогою невеликої кількості JavaScript:
Швидке пояснення:
- Спеціальна властивість використовується для відстеження прогресу анімації.
- Анімація «перезапускається», якщо встановити прапорець.
- Код JS обчислює правильні
animation-delay
щоб забезпечити плавний перехід. я рекомендую цю статтю якщо ви не знайомі з від’ємними значеннямиanimation-delay
.
Ви можете розглядати це рішення як гібрид «перезапуску анімації» та підходу «перемикання передач».
Тут важливо правильно відслідковувати хід анімації. Обхідні шляхи можливі, якщо @property
недоступний. Як приклад, ця версія використовує z-index
щоб відстежувати прогрес:
Додаткова примітка: спочатку я також намагався створити версію лише для CSS, але не вдалося. Хоча я не впевнений на 100%, я думаю, що це тому animation-delay
is не анімований.
Ось версія з мінімальним JavaScript. Працює лише «введення slowmo».
Будь ласка, дайте мені знати, якщо вам вдасться створити робочу версію лише для CSS!
Уповільнена будь-яка анімація (з JS)
Нарешті, я хотів би поділитися рішенням, яке працює для (майже) будь-якої анімації, навіть із кількома складними @keyframes
:
По суті, вам потрібно додати трекер прогресу анімації, а потім ретельно обчислити animation-delay
для нової анімації. Однак іноді може бути складно (але можливо) отримати правильні значення.
Наприклад:
animation-timing-function
НЕlinear
.animation-direction
НЕnormal
.- кілька значень в
animation-name
з різнимиanimation-duration
і сanimation-delay
с.
Цей спосіб також описаний тут для API веб-анімації.
Подяки
Я почав йти цим шляхом після того, як зіткнувся з проектами лише на CSS. Деякі були делікатний твір мистецтва, а деякі були складні пристосування. Мої улюблені ті, що включають 3D-об’єкти, наприклад, це стрибаючий м'яч і це пакувальний куб.
На початку я поняття не мав, як це було зроблено. Пізніше я прочитав і багато чому навчився цей гарний підручник Ани Тудор.
Як виявилося, створення та анімація 3D-об'єктів за допомогою CSS мало чим відрізняється від того, як це робити за допомогою змішувач, тільки з трохи іншим смаком.
Висновок
У цій статті ми розглянули поведінку CSS-анімації, коли an animate-*
майно змінено. Особливо ми розробили рішення для «повторного відтворення анімації» та «уповільненої анімації».
Сподіваюся, ця стаття буде вам цікава. Будь ласка, дай мені знати твої думки!