В современном веб-дизайне анимация играет важную роль, делая интерфейсы более привлекательными и удобными для пользователей. Одним из элементов, где анимация может быть особенно эффектна, является таймер.
Представьте, например, сайт онлайн-школы, где таймер отсчитывает время до начала вебинара. Или интернет-магазин, где анимированный таймер создает эффект срочности и подталкивает к покупке во время распродажи.
В этой статье мы создадим простой таймер с плавной анимацией цифр с помощью чистого JavaScript и CSS. Мы разберем код пошагово, объясним назначение каждой его части и проанализируем ошибки, которые возникали в процессе разработки.
Начнем с создания базовой HTML структуры нашего таймера. Нам понадобятся контейнеры для цифр, двоеточие и кнопки управления:
<div class="timer-container" id="timer-container">
<div class="timer-display">
<div class="digit" id="minutes-tens">
<span class="digit-inner">0</span>
</div>
<div class="digit" id="minutes-ones">
<span class="digit-inner">0</span>
</div>
<span class="digit">:</span>
<div class="digit" id="seconds-tens">
<span class="digit-inner">0</span>
</div>
<div class="digit" id="seconds-ones">
<span class="digit-inner">0</span>
</div>
</div>
<div class="buttons">
<button id="startTimer">Start</button>
<button id="stopTimer">Stop</button>
</div>
</div>
Разбор HTML кода:
timer-container: Основной контейнер, объединяющий все элементы таймера. Мы присвоили ему ID timer-container для удобства доступа через JavaScript.timer-display: Контейнер для отображения цифр таймера.digit: Контейнер для каждой отдельной цифры таймера. Обратите внимание на уникальные ID для каждого контейнера цифры (minutes-tens, minutes-ones и т.д.). Это позволит нам легко обращаться к ним из JavaScript и обновлять их значения.digit-inner: Внутренний элемент <span>, в котором будет отображаться сама цифра. Мы будем использовать этот элемент для создания анимации смены цифр.buttons: Контейнер для кнопок "Start" и "Stop", которые позволят пользователю управлять таймером.startTimer и stopTimer: ID кнопок "Start" и "Stop" соответственно.Теперь перейдем к CSS стилям нашего таймера. Мы определим внешний вид таймера, а также создадим анимацию смены цифр с помощью keyframes:
.timer-container {
padding: 20px 30px;
border-radius: 2rem;
display: flex;
align-items: center;
gap: 15px;
background-color: #87ceeb;
color: #000;
transition: background-color 1s ease-in-out, color 1s ease-in-out;
}
/* ... (другие стили для .timer-container.night, .buttons button, .buttons button:hover) ... */
.timer-display {
display: flex;
font-size: 24px;
/* gap: 4px; */
padding:0.5rem;
border-radius:2rem;
background-color:rgba(0,0,0,0);
backdrop-filter:blur(5px);
}
.digit {
display: inline-block;
position: relative;
overflow: hidden;
width: 20px;
text-align: center;
}
.digit-inner {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
line-height: 32px;
}
/* Анимация цифр через keyframes */
.digit-enter {
animation: digitEnter 0.4s ease-in-out forwards;
}
.digit-exit {
animation: digitExit 0.5s ease-in-out forwards;
}
@keyframes digitEnter {
0% {
transform: translateY(100%);
opacity: 0;
filter:blur(5px);
}
80% {
transform: translateY(-5%);
opacity: 1;
filter:blur(0px);
}
90% {
transform: translateY(5%);
opacity: 1;
filter:blur(0px);
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes digitExit {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
/* ... (другие стили для day-night, day-night-inner, анимации dayNightEnter и dayNightExit) ... */
Разбор CSS кода:
.timer-container, .timer-container.night, .buttons button, .buttons button:hover, .timer-display, .digit: Эти классы определяют базовые стили элементов таймера (цвета, размеры, отступы, шрифты и т.д.). Мы используем переходы (transition), чтобы изменения стилей, например, при смене дня и ночи, происходили плавно..digit-inner: Этот класс относится к элементу <span>, который отображает цифру. position: absolute позволяет нам позиционировать цифру абсолютно внутри ее контейнера (.digit), а left, top, width и height задают размер и положение цифры так, чтобы она занимала все пространство контейнера. .digit-enter и .digit-exit: Эти классы определяют анимацию появления и исчезновения цифр соответственно. @keyframes digitEnter и @keyframes digitExit: Здесь определяются сами анимации. digitEnter анимирует появление цифры снизу вверх, а digitExit — исчезновение цифры вверх. Обратите внимание на использование transform: translateY(), opacity и filter: blur(), чтобы создать эффект плавного появления и исчезновения с размытием..day-night, .day-night-inner, @keyframes dayNightEnter, @keyframes dayNightExit) аналогичны стилям для цифр и создают плавную анимацию смены дня и ночи с размытием.Теперь напишем JavaScript код, который будет управлять логикой нашего таймера и анимацией:
const digitContainers = {
'minutes-tens': document.getElementById('minutes-tens'),
'minutes-ones': document.getElementById('minutes-ones'),
'seconds-tens': document.getElementById('seconds-tens'),
'seconds-ones': document.getElementById('seconds-ones')
};
const startButton = document.getElementById('startTimer');
const stopButton = document.getElementById('stopTimer');
const dayNightDisplay = document.getElementById('day-night');
const timerContainer = document.getElementById('timer-container');
const body = document.body;
// Константы для классов
const CLASS_DAY = 'day';
const CLASS_NIGHT = 'night';
const CLASS_DAY_NIGHT_INNER = 'day-night-inner';
const CLASS_DAY_NIGHT_ENTER = 'day-night-enter';
const CLASS_DAY_NIGHT_EXIT = 'day-night-exit';
const CLASS_DIGIT_INNER = 'digit-inner';
const CLASS_DIGIT_ENTER = 'digit-enter';
const CLASS_DIGIT_EXIT = 'digit-exit';
// Константы для цветов
const COLOR_DAY = 'url(https://ti-web.ru/assets/templ/img/noise.png), radial-gradient(at center, #6C7DD9 0%, #ABE4FF 100%)';
const COLOR_NIGHT = 'url(https://ti-web.ru/assets/templ/img/noise.png), radial-gradient(at center, #3F54E1 0%, #4257EE 20%, #050D1A 100%)';
let seconds = 0;
let timerInterval;
const dayNightDuration = 10;
function updateTimer() {
seconds++;
let minutes = Math.floor(seconds / 60);
let remainingSeconds = seconds % 60;
updateDigitIfNeeded('minutes-tens', Math.floor(minutes / 10));
updateDigitIfNeeded('minutes-ones', minutes % 10);
updateDigitIfNeeded('seconds-tens', Math.floor(remainingSeconds / 10));
updateDigitIfNeeded('seconds-ones', remainingSeconds % 10);
updateDayNight();
}
function updateDigitIfNeeded(digitId, value) {
const digitContainer = digitContainers[digitId];
const currentDigit = digitContainer.querySelector(`.${CLASS_DIGIT_INNER}`);
if (currentDigit.textContent === value.toString()) {
return;
}
// Используем innerHTML для очистки контейнера
digitContainer.innerHTML = '';
const newDigit = document.createElement('span');
newDigit.classList.add(CLASS_DIGIT_INNER, CLASS_DIGIT_ENTER);
newDigit.textContent = value;
digitContainer.appendChild(newDigit);
}
function updateDayNight() {
if (seconds % dayNightDuration === 0) {
const isDay = dayNightDisplay.querySelector(`.${CLASS_DAY}`) !== null;
const newDayNight = isDay ? CLASS_NIGHT : CLASS_DAY;
// Используем innerHTML для очистки контейнера
dayNightDisplay.innerHTML = '';
// Создаем новый элемент day-night-inner
const newDayNightInner = document.createElement('span');
newDayNightInner.id = "day-night-current";
newDayNightInner.classList.add(CLASS_DAY_NIGHT_INNER, newDayNight, CLASS_DAY_NIGHT_ENTER);
newDayNightInner.textContent = isDay ? 'Ночь' : 'День';
dayNightDisplay.appendChild(newDayNightInner);
timerContainer.classList.toggle(CLASS_DAY);
timerContainer.classList.toggle(CLASS_NIGHT);
body.style.background = isDay ? COLOR_NIGHT : COLOR_DAY;
}
}
function resetTimer() {
clearInterval(timerInterval);
seconds = 0;
for (const digitId in digitContainers) {
updateDigitIfNeeded(digitId, 0);
}
// Сбрасываем day-night
dayNightDisplay.innerHTML = ''; // Используем innerHTML для очистки
const newDayNightInner = document.createElement('span');
newDayNightInner.id = "day-night-current";
newDayNightInner.classList.add(CLASS_DAY_NIGHT_INNER, CLASS_DAY);
newDayNightInner.textContent = 'День';
dayNightDisplay.appendChild(newDayNightInner);
timerContainer.classList.remove(CLASS_NIGHT);
timerContainer.classList.add(CLASS_DAY);
body.style.background = COLOR_DAY;
}
startButton.addEventListener('click', () => {
clearInterval(timerInterval);
seconds = 0;
updateTimer();
timerInterval = setInterval(updateTimer, 1000);
});
stopButton.addEventListener('click', resetTimer);
</script>
</body>
</html>
Разбор JavaScript кода:
digitContainers: Объект для хранения ссылок на контейнеры цифр по их ID (minutes-tens, minutes-ones, seconds-tens, seconds-ones). Это позволяет легко обращаться к контейнерам цифр и обновлять их содержимое.startButton, stopButton, dayNightDisplay, timerContainer, body: Переменные, содержащие ссылки на HTML-элементы, с которыми мы будем взаимодействовать в JavaScript.CLASS_DAY, CLASS_NIGHT, CLASS_DAY_NIGHT_INNER, CLASS_DAY_NIGHT_ENTER, CLASS_DAY_NIGHT_EXIT, CLASS_DIGIT_INNER, CLASS_DIGIT_ENTER, CLASS_DIGIT_EXIT: Константы, хранящие имена CSS-классов. Использование констант делает код более читаемым и легким в поддержке. COLOR_DAY, COLOR_NIGHT: Константы для хранения цветов фона в дневном и ночном режимах. seconds, timerInterval, dayNightDuration: Переменные для хранения текущего времени в секундах, интервала таймера и длительности дня и ночи в секундах (в данном случае 10 секунд).Функции:
updateTimer: seconds).updateDigitIfNeeded для обновления каждой цифры таймера.updateDayNight для проверки и смены дня и ночи.updateDigitIfNeeded: digitId) и новое значение цифры (value). currentDigit) в указанном контейнере.digitContainer.innerHTML = '';).<span> (newDigit) с новой цифрой. CLASS_DIGIT_INNER, CLASS_DIGIT_ENTER) к новому элементу для анимации появления.updateDayNight: seconds) на длительность дня/ночи (dayNightDuration).day-night (dayNightDisplay.innerHTML = '';). <span> (newDayNightInner) с текстом "День" или "Ночь" в зависимости от текущего состояния. CLASS_DAY_NIGHT_INNER, CLASS_DAY или CLASS_NIGHT, CLASS_DAY_NIGHT_ENTER) к новому элементу для анимации.day-night. CLASS_DAY и CLASS_NIGHT у контейнера таймера (timerContainer) для смены его цветовой схемы. body.style.background).resetTimer: clearInterval(timerInterval);).updateDigitIfNeeded для каждого контейнера цифр, устанавливая значение цифры в 0, чтобы сбросить отображение таймера.day-night в начальное состояние («День»), очищая его содержимое и добавляя новый элемент с соответствующим текстом и классом. Обработчики событий:
startButton.addEventListener('click', ...): При нажатии на кнопку "Start":setInterval, вызывая функцию updateTimer каждую секунду.stopButton.addEventListener('click', ...): При нажатии на кнопку "Stop" вызывается функция resetTimer, которая сбрасывает таймер в начальное состояние.
Создание нашего анимированного таймера не было лишено сложностей. На пути к плавной и корректной анимации мы столкнулись с несколькими ошибками и проблемами:
Проблема 1: Наложение цифр. В начале мы пытались удалять старый элемент <span> цифры сразу же после создания и добавления нового элемента. Это приводило к тому, что новая цифра начинала анимацию появления, в то время как старая цифра еще не завершила анимацию исчезновения, что создавало эффект наложения.
Проблема 2: Ошибки Failed to execute 'removeChild' on 'Node'. Еще одной проблемой стало некорректное использование метода removeChild, который вызывал ошибку, если элемент, который мы пытались удалить, уже был удален из DOM.
Решение: Мы поняли, что ключ к решению обеих проблем — правильное управление временем и порядком действий. Мы перестали использовать removeChild, заменив его на innerHTML = '' для очистки контейнеров перед добавлением новых элементов. Кроме того, мы внесли изменения в функцию updateDigitIfNeeded, чтобы новый элемент добавлялся в контейнер только после завершения анимации исчезновения старого элемента.
Создание анимированного таймера — задача не из простых, но она помогает лучше понять принципы работы DOM и анимации в JavaScript. Важно помнить, что корректная работа анимации зависит от правильного управления временем и последовательностью действий.