furry.cat 06 февраля 2020

Нет времени объяснять! Пишем таймер обратного отсчёта на чистом CSS

Ради фана реализуем динамический виджет таймера на одних стилях. Для порядка приводим ещё и решение на JavaScript (но это не главное).
0
3064

CSS – мощный инструмент современного разработчика. Он многое умеет, в нём есть переменные, функции, наследование и ещё много крутых штук. Но всё-таки это не язык программирования – у него совсем другая сфера ответственности. Тем интереснее использовать CSS для решения задач программирования :) Именно этим сегодня и займёмся.

Дисклеймер! Многие вещи в принципе невозможно сделать на CSS. Ещё больше вещей делать на CSS нерационально. Мы занимаемся этим только из болезненного любопытства и стремления познать все скрытые возможности инструмента.

Условие задачи

Нужно сделать таймер обратного отсчёта. Предъявляемые требования:

  • Таймер должен выводить миллисекунды от 99 до 0.
  • Когда остаётся меньше 10 миллисекунд, нужно выводить только одну цифру (от 9 до 0) и центрировать её.
  • Бонус #1: Цвет шрифта и фона можно настраивать в процессе работы (без кусочка JS не обойтись).
  • Бонус #2. После остановки таймера его можно перезапустить.
  • Код должен работать и на ПК, и на мобильных устройствах.

Чтобы выполнить указанные условия, пойдём напролом. Все нужные цифры (от 0 до 9) запишем прямо в базовую разметку страницы. Затем для имитации таймера анимируем их в нужном ритме и правильной последовательности.

Да, не очень элегантно. Но сработает, вот увидите.

Что нам потребуется?

  1. CSS трансформации
  2. CSS анимации
  3. Flexbox-модель
  4. CSS переменные
  5. Различные селекторы

Вот что получится в итоге:

Реализация на JavaScript

Сразу посмотрим, как это можно было сделать на JS.

Простой и понятный код, состоящий из пары функций. Для обновления таймера подписываемся на момент перерисовки браузера с помощью метода window.requestAnimationFrame().

        let end;
const now = Date.now;
const timer = document.getElementById("timer");
const duration = 9900;

function displayCountdown() {
  const count = parseInt((end - now()) / 100);
  timer.textContent =
    count > 0 ? (window.requestAnimationFrame(displayCountdown), count) : 0;
}

function start() {
  end = now() + duration;
  window.requestAnimationFrame(displayCountdown);
}
    

Всего пара блоков в HTML:

        <div class="timer-container">
  <p class="timer" id="timer">99</p>
</div>
    

И элементарные стили для выравнивания:

        .timer-container {
  display: flex;
  height: 100vh; 
}

.timer {
  margin: auto;
}
    

Согласитесь, ровным счётом ничего интересного. Поэтому бросаем эту ерунду и идём писать по-настоящему крутой таймер.

Общий подход

Сложно придумать более прямое решение, чем простое перечисление в HTML всех цифр. Расположим их в две группы (два разряда). По мере необходимости будем скрывать ненужные символы.

        <div class="timer">
  <div class="digit seconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span>
    <span>0</span> 
  </div><div class="digit milliseconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span> 
    <span>0</span>
  </div>
</div>
    

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

CSS трансформации

Большинство CSS-свойств плохо подходят для анимирования, так как их изменение вызывает перерисовку страницы. Но есть два «безопасных» свойства, которыми мы можем воспользоваться: transform и opacity.

Подробнее об анимации в вебе можно прочитать в замечательном руководстве High Performance Animations.

Для оживления таймера возьмём надёжное свойство translateY. Оно обеспечит перемещение блока только по y-оси.

        .selector {
  transform: translateY(0);
}
    

Можно воспользоваться и полным свойством translate, но помните, что его первый аргумент соответствует x-координате. Если хотите перемещать элемент только по вертикали, то передайте первым параметром 0.

        .selector {
  transform: translate(3em);
}

/* то же самое */
.selector {
  transform: translate(3em, 0);
}
    

Чтобы лучше понять функции трансформации, загляните в спецификацию CSS Transforms Module Level 1. Там всё разобрано на подробных примерах, так что вы разберётесь, даже если не очень любите математику.

CSS animations

Следующий шаг – анимировать применение трансформаций. Для этого мы используем CSS-анимации.

Самое важное правило, которое вы должны знать, – это @keyframes. Оно позволяет разбить анимацию на кадры и описать каждый из них в отдельности.

        @keyframes seconds {
  0% { transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { 
    transform: translateY(-10em);
    width: 0;
  }
  100% { 
    transform: translateY(-10em);
    width: 0;
  }
}

@keyframes milliseconds {
  0% {transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { transform: translateY(-9em) }
  100% { transform: translateY(-9em) }
}
    

Здесь мы создали две анимации – по одной для каждого блока с цифрами.

Обратите внимание на два последних кадра в анимации первого блока. В этот момент там должна отображаться цифра 0, но она нам не нужна, поэтому скрываем её с помощью width: 0.

Чтобы применить анимации, используем краткий синтаксис свойства animation:

        .seconds {
  animation: seconds 10s 1 step-end forwards;
}

.milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
    

Если вы зайдете в панель инструментов разработчика и откроете вкладку с вычисленными значениями (computed), то увидите, что вместо одного свойства animation к элементу применились сразу несколько:

animation-name

Имя анимации, использующееся для её идентификации. Для него можно использовать латинские буквы, цифры, нижнее подчёркивание и дефисы. Первой должна идти буква. В начале не могут стоять зарезервированные слова none, unset, initial или inherit, а также сочетание --. Регистр символов имеет значение.

animation-duration

Продолжительность одного цикла анимации. Для первой колонки цифр анимация будет длиться 10 секунд (10s). Вторая колонка двигается в 10 раз быстрее (1s).

animation-iteration-count

Количество циклов анимации, которое должно выполниться до ее остановки. Первую колонку нужно прокрутить лишь один раз – от 9 до 0. Вторую – целых 10 раз, по одному на каждое положение первой колонки.

animation-timing-function

Это свойство описывает прогресс анимации в течение одного цикла. Если вы знакомы с кривыми Безье, то можете контролировать его до мельчайших подробностей с помощью функции cubic-bezier(). Для простых смертных есть несколько готовых значений animation-timing-function, обозначенных ключевыми словами (ease, ease-in и т.д)

Нам же больше подойдёт значение step-end. Это то же самое, что и steps(1, jump-end).

Функция steps() разбивает анимацию на равные «шаги», то есть величина изменяется не плавно, а прерывисто. Первый аргумент – количество шагов, второй – момент, когда начинается анимация. Ключевое слово jump-end означает, что анимация запускается в конце, а не начале каждого шага.

Чтобы лучше разобраться в этой функции, обратитесь к статье Дэна Уилсона Jumps: The New Steps() in Web Animation.

animation-fill-mode

Состояние целевого объекта до и после завершения анимации. Нам требуется, чтобы колонки останавливались на последней цифре (последний ключевой кадр), поэтому используем значение forwards.

Когда анимация остановится, первая цифра будет скрыта, а вторая колонка замрёт на позиции -9em.

Ещё больше о CSS анимациях вы можете узнать в спецификации CSS Animations Level 1.

Flexbox

Цифру 0 в первом разряде мы уже скрыли с помощью инструкции @keyframes, осталось только выровнять таймер по центру страницы:

        .timer-container {
  display: flex;
  height: 100vh; 
}

.timer {
  overflow: hidden;
  margin: auto;
  height: 1em;
  width: 2ch;
  text-align: center;
}

.digit {
  display: inline-block;
}

.digit span {
  display: block;
  width: 100%;
  height: 1em;
}
    

Для вертикального выравнивания используем автоматический расчёт маргинов у потомка флекс-контейнера. Другими словами, назначаем display: flex родительскому блоку, и margin: auto дочернему.

Горизонтальное выравнивание достигается обычным text-align: center.

Больше информации о Flex-модели – в спецификации CSS Flexible Box Layout Module Level 1.

Бонус #1: Динамическое изменение цвета

В условиях задачи была также бонусная возможность кастомизации цвета текста и фона нашего таймера.

Используем для ее решения кастомные свойства CSS и инпут выбора цвета.

Положим цвета в переменные:

        :root {
  --fontColour: #000000;
  --bgColour: #ffffff;
}
    

И используем их в таймере:

        .timer {
  background-color: var(--bgColour, white);
}

.digit {
  color: var(--fontColour, black);
}
    

Добавим на страницу два инпута для изменения цветовой схемы:

        <aside>
  <label>
    <span>Font colour:</span>
    <input id="fontColour" type="color" value="#000000" />
  </label>
  <label>
    <span>Background colour:</span>
    <input id="bgColour" type="color" value="#ffffff" />
  </label>
</aside>
    

Теперь придётся написать пару строк JS-кода, чтобы динамически изменять переменные цветов при изменении инпутов:

        let root = document.documentElement;
const fontColourInput = document.getElementById('fontColour');
const bgColorInput = document.getElementById('bgColour');

fontColourInput.addEventListener('input', updateFontColour, false);
bgColorInput.addEventListener('input', updateBgColour, false);

function updateFontColour(event) {
  root.style.setProperty('--fontColour', event.target.value);
}

function updateBgColour(event) {
  root.style.setProperty('--bgColour', event.target.value);
}
    

Да, не всё можно решить на чистом CSS.

Бонус #2: Перезапуск таймера

Сейчас таймер отрабатывает только один раз. Чтобы запустить его сначала, необходимо перезагрузить страницу. Давайте добавим возможность перезапуска.

Возможно, вы подумали, что нам придётся снова смошенничать и добавить JavaScript? А вот и нет, мы справимся своими силами!

Воспользуемся чекбоксом, который обладает состоянием checked. Мы легко можем получить к нему доступ из CSS.

        .toggle span {
  font-size: 1.2em;
  padding: 0.5em;
  background-color: palegreen;
  cursor: pointer;
  border-radius: 4px;
}

input[type="checkbox"] {
  opacity: 0;
  position: absolute;
}

 input[type="checkbox"]:checked ~ aside .toggle span:first-of-type {
  display: none;
}

.toggle span:nth-of-type(2) {
  display: none;
}

input[type="checkbox"]:checked ~ aside .toggle span:nth-of-type(2) {
  display: inline;
}
    

Теперь анимация работает только при включенном чекбоксе, а при выключенном таймер сбрасывается и возвращается в исходное состояние.

        input[type="checkbox"]:checked ~ .timer .seconds {
  animation: seconds 10s 1 step-end forwards;
}

input[type="checkbox"]:checked ~ .timer .milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
    

Важно, чтобы чекбокс находился на одном уровне с таймером и стоял в разметке перед ним. Это даст нам возможность воспользоваться селектором "сиблингов" (~). А лейбл для него может находиться где угодно.

***

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

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

Если вы любите CSS, вот ещё кое-что:

В комментариях делитесь самыми нестандартными решениями ваших задач.

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Front-end разработчик в Tilda
по итогам собеседования
IOS developer
Москва, от 150000 RUB до 220000 RUB
Middle/Senior Java Developers
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG