Создаем анимированный таймер на JavaScript и CSS

Вступление

В современном веб-дизайне анимация играет важную роль, делая интерфейсы более привлекательными и удобными для пользователей. Одним из элементов, где анимация может быть особенно эффектна, является таймер.

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

В этой статье мы создадим простой таймер с плавной анимацией цифр с помощью чистого JavaScript и CSS. Мы разберем код пошагово, объясним назначение каждой его части и проанализируем ошибки, которые возникали в процессе разработки.

HTML структура таймера

Начнем с создания базовой 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 стили и анимация

Теперь перейдем к 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 логика таймера

Теперь напишем 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:
    • Принимает ID контейнера цифры (digitId) и новое значение цифры (value).
    • Находит текущий элемент цифры (currentDigit) в указанном контейнере.
    • Если текущее значение цифры совпадает с новым значением, функция завершается, не производя обновления.
    • Если значение цифры изменилось:
      • Очищает контейнер цифры (digitContainer.innerHTML = '';).
      • Создает новый элемент <span> (newDigit) с новой цифрой.
      • Добавляет необходимые CSS классы (CLASS_DIGIT_INNER, CLASS_DIGIT_ENTER) к новому элементу для анимации появления.
      • Добавляет новый элемент в контейнер цифры.
  • updateDayNight:
    • Проверяет, нужно ли сменить день на ночь или наоборот, анализируя остаток от деления текущего времени (seconds) на длительность дня/ночи (dayNightDuration).
    • Очищает контейнер day-night (dayNightDisplay.innerHTML = '';).
    • Создает новый элемент <span> (newDayNightInner) с текстом "День" или "Ночь" в зависимости от текущего состояния.
    • Добавляет необходимые CSS классы (CLASS_DAY_NIGHT_INNER, CLASS_DAY или CLASS_NIGHT, CLASS_DAY_NIGHT_ENTER) к новому элементу для анимации.
    • Добавляет новый элемент в контейнер day-night.
    • Переключает CSS классы CLASS_DAY и CLASS_NIGHT у контейнера таймера (timerContainer) для смены его цветовой схемы.
    • Изменяет цвет фона страницы (body.style.background).
  • resetTimer:
    • Останавливает таймер, очищая интервал (clearInterval(timerInterval);).
    • Сбрасывает счетчик секунд в 0.
    • Вызывает updateDigitIfNeeded для каждого контейнера цифр, устанавливая значение цифры в 0, чтобы сбросить отображение таймера.
    • Сбрасывает блок day-night в начальное состояние («День»), очищая его содержимое и добавляя новый элемент с соответствующим текстом и классом.
    • Устанавливает начальные стили для контейнера таймера и фона страницы.
  • Обработчики событий:

    • startButton.addEventListener('click', ...): При нажатии на кнопку "Start":
      • Останавливается любой запущенный таймер.
      • Счетчик секунд сбрасывается в 0.
      • Запускается таймер с помощью setInterval, вызывая функцию updateTimer каждую секунду.
    • stopButton.addEventListener('click', ...): При нажатии на кнопку "Stop" вызывается функция resetTimer, которая сбрасывает таймер в начальное состояние.

    Путь создания таймера

    Создание нашего анимированного таймера не было лишено сложностей. На пути к плавной и корректной анимации мы столкнулись с несколькими ошибками и проблемами:

    Проблема 1: Наложение цифр. В начале мы пытались удалять старый элемент <span> цифры сразу же после создания и добавления нового элемента. Это приводило к тому, что новая цифра начинала анимацию появления, в то время как старая цифра еще не завершила анимацию исчезновения, что создавало эффект наложения.

    Проблема 2: Ошибки Failed to execute 'removeChild' on 'Node'. Еще одной проблемой стало некорректное использование метода removeChild, который вызывал ошибку, если элемент, который мы пытались удалить, уже был удален из DOM.

    Решение: Мы поняли, что ключ к решению обеих проблем — правильное управление временем и порядком действий. Мы перестали использовать removeChild, заменив его на innerHTML = '' для очистки контейнеров перед добавлением новых элементов. Кроме того, мы внесли изменения в функцию updateDigitIfNeeded, чтобы новый элемент добавлялся в контейнер только после завершения анимации исчезновения старого элемента.

    Заключение

    Создание анимированного таймера — задача не из простых, но она помогает лучше понять принципы работы DOM и анимации в JavaScript. Важно помнить, что корректная работа анимации зависит от правильного управления временем и последовательностью действий.


    07.26.24 в 03:53