Аналоговые часы, CSS и ничего больше

Что здесь не так?


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


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


Вкладка Rendering в консоли Chrome помогла подтвердить факт перерисовки:





Мы видим перерисовку каждую секунду и средний fps 50 кадров и это на странице, где у нас изображены всего одни часы.


А как еще можно?


И так я поставил перед собой задачу создать аналоговые часы, который не будут вызывать перерисовку страницы и полагаться на JavaScript для вычисления положения стрелки. Часы которые не будут никак влиять на частоту кадров в браузере. Среди всех мне известных CSS свойств, только одно позволяет трансформировать элемент без перерисовки — transform. Значит его и будем использовать.

Но сначала создадим свои часы, с которыми нам будем удобно шевелить стрелку одним только transform свойством. Создадим циферблат со всеми стрелками:



Идея тут такая — если поместить каждую стрелку в отдельный контейнер, который будет в центре часов, то вращая этот контейнер относительно центра мы будем вращать и саму стрелку. Вот пример стилей такого контейнера для часовой стрелки:

.clock__hand {
    margin-left: -0.5em;
    margin-top: -0.5em;
    font-size: inherit;
    position: absolute;
    display: block;
    height: 1em;
    width: 1em;
    left: 50%;
    top: 50%;
}
.clock__hand--hour::after {
    content: "";
    border-radius: 0.015em 0.015em 0.01em 0.01em;
    background-color: #000;
    margin-bottom: -0.02em;
    margin-left: -0.025em;
    font-size: inherit;
    position: absolute;
    display: block;
    height: 0.25em;
    width: 0.05em;
    bottom: 50%;
    left: 50%;
}

С помощью font-size в данном случае мы задаем размер циферблата и всех компонентов часов.

Код вида самых часов.


Хорошо, а на сколько вращать?


Вращать стрелку на все 360 градус. Правильный вопрос: как долго ее вращать? А зависит это от того, какую стрелку мы вращаем. Часовую — 12 часов, минутную — час, секундную — минуту.

.clock__hand--hour {
    animation: clock-hand-rotate 43200s linear infinite;
}
.clock__hand--minute {
    animation: clock-hand-rotate 3600s linear infinite;
}
.clock__hand--second {
    animation: clock-hand-rotate 60s linear infinite;
}
@keyframes clock-hand-rotate {
    from {
        transform: rotate(0deg)
    }
    to {
        transform: rotate(360deg)
    }
}

И так наши часы заработали.

А что же нам скажет про них Chrome?



Никаких перерисовок и стабильных 60 fps


Но это же не наше время.


И так наше начальное время 00:00:00 потому что все стрелки начинают анимацию с 0 градусов. Чтобы начинать с настоящего времени нам нужно рассчитывать начальный градус отдельно для каждой стрелки относительно времени. И так у нас два варианта, либо на стороне сервера рендерить CSS относительно времени запроса, либо использовать JavaScript. Конечно сервер рендеринг и без скриптовые часы это круто, но ради подтверждения концепта мы все же используем JavaScript.

var date = new Date(),
    hours = date.getHours(),
    minutes = date.getMinutes(),
    seconds = date.getSeconds();

if (hours > 12) {
    hours -= 12;
}

var secondsStartDegree = 360 / 60 * seconds,
    minutesStartDegree = 360 / 60 * minutes + 6 / 60 * seconds,
    hoursStartDegree = 360 / 12 * hours + 30 / 60 * minutes + 0.5 / 60 * seconds;

var style = document.createElement('style');

style.type = 'text/css';
style.innerHTML = '\
        @keyframes clock-hand-rotate--hour {\
            from {transform: rotate(' + hoursStartDegree + 'deg)}\
            to {transform: rotate(' + (hoursStartDegree + 360) + 'deg)}\
        }\
        @keyframes clock-hand-rotate--minute {\
            from {transform: rotate(' + minutesStartDegree + 'deg)}\
            to {transform: rotate(' + (minutesStartDegree + 360) + 'deg)}\
        }\
        @keyframes clock-hand-rotate--second {\
            from {transform: rotate(' + secondsStartDegree + 'deg)}\
            to {transform: rotate(' + (secondsStartDegree + 360) + 'deg)}\
        }\
        .clock__hand--hour {\
            animation: clock-hand-rotate--hour 43200s linear infinite;\
        }\
        .clock__hand--minute {\
            animation: clock-hand-rotate--minute 3600s linear infinite;\
        }\
        .clock__hand--second {\
            animation: clock-hand-rotate--second 60s steps(60) infinite;\
        }';

document.getElementsByTagName('head')[0].appendChild(style);

И там мы создаем 3 анимации для каждой из стрелок и подключаем их к соответственным классам.

Вот результат.

А что еще можно?


Ну если вам не хватает того факта, что у вас часы которые почти не забирают ресурсов браузера. То вот еще пару фишек:


Хорошо а в чем минусы


Их не так уж много, но они есть:

  • Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать. Но все это можно поправить заново задав keyframe.
  • Не всюду сайт будет выглядеть идеально, вот например в том же IE11 некоторые центры немного смещены:


Итоги


Не знаю как на счет использования этих часов в продакшене, но я сделал для себя лично выводы относительно новых технологий.

И я бы сказал, что не всегда стоит кидаться за новыми технологиями, если и старые еще могут не мало. А самое главное — лучше хорошо использовать надежные инструменты, чем просто пробовать всюду всунуть новую невероятную фичу языка.


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


Надеюсь вам тоже было интересно, спасибо за прочтение.

Поделиться публикацией
Комментарии 17
    +2
    CSS и ничего больше

     


    document.getElementsByTagName('head')[0].appendChild(style);

    Это что-то вроде ничего_больше.js?

      0
      JavaScript был использован только для инициализации часов, создания начальных стилей. Сами часы идут только на основе CSS анимации.
      Но если вас интересует только CSS+HTML часы — можно перенести создание CSS анимации на серверную часть, к примеру можно сгенерировать те же стили при помощи PHP, тогда на стороне клиента все будет работать даже если отключены скрипты.
      Я кстати об этом упоминал в статье:
      И так у нас два варианта, либо на стороне сервера рендерить CSS относительно времени запроса, либо использовать JavaScript. Конечно сервер рендеринг и без скриптовые часы это круто, но ради подтверждения концепта мы все же используем JavaScript.

      Простите, что не уточнил сразу в статье.
        +1

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

          +1
          Я с вами согласен. Но все о чем мы сейчас спорим есть в статье. Возможно проблема в том, что я не раскрыл эти проблемы полностью, тогда извиняюсь, это моя первая публичная статья и я постараюсь следующие сделать лучше, возможно даже стоит поправить эту.
          Но, лично у меня пока сложилось впечатление, что вы пробежались по статье не вчитываясь в детали.

          На счет нагрузок уже отписывал, но все же
          Я не знаю как себя поведет CSS при нагрузках процессора, я поэксперементирую с этим и сообщу вам результат. Но, хорошо сделанная страница не должна нагружать поток на столько, чтобы часы сильно отставали (больше 5 секунд разницы в час), а такими малыми отклонениями можно пренебречь.
          Также, ради эксперимента оставлял вкладку с часами открытой, пока было запущено пару тяжелых процессов и еще 20 вкладок и часы за 2 часа так и не отстали.
          В случае сна и гибернации — ничего не гарантирую. Но можно переопределять CSS классы при возвращении пользователя на страницу, много проблем это не вызовет.

          Сама статья — это все лишь концепт, а не готовое решение.

      0
      Интересно, спасибо большое.
        0
        Ещё забавная реализация часов без клиентского (только gif) рендеринга:
        bolknote.ru/all/3300
          +1
          средний fps 50 кадров

          Если часы обновляются раз в секунду, откуда вообще может взяться 50 кадров в секунду?
          Чтобы измерить быстроту отрисовки, нужно поставить постоянную перерисовку часов, например вот так:


          // в setTimeVariables
          setCustomProperty('seconds', time.getSeconds() + time.getMilliseconds() / 1000);
          
          const start = () => {
              setTime()
              requestAnimationFrame(start)
          }
          
          start()

          Тогда можно увидеть стабильные 60фпс (или сколько частота обновления на мониторе)
          Также, если поставить стрелкам will-change: transform, то они не будет репайнта на каждый кадр.
          Так что css-переменные тут вообще не причем.

            0
            Поддерживаю. В статье изложены неверные выводы. Переменные вообще никак к отрисовке не относятся и сами по себе ее не вызывают. Просто в одном варианте часов отдельный слой создается при каждом сдвиге стрелки, а во втором варианте слой существует постоянно.
            Для плавности же можно просто добавить тех же стилей вроде `transition: 1s linear;`. В итоге код в статье сильно усложнился / запутался, а прибавилось только минусов. %)
              0
              Не совсем, не забывайте, что вы вызываете функцию каждую секунду, и если вам это кажется мелочью — придставте если их будет 10, а может и больше. И да, сами CSS переменные — прекрасный инструмент и никаких нагрузок не вызывают, но может все же не стоит переносить на CSS переменные и JS то, с чем сам по себе справляются CSS?

              Также, как человек, который всегда на проектах имеет дело с IE 11 и старше, для меня очень важна поддержка старых браузеров и высокая производительность, чего собственно я и добивался в этой статье.
                +1
                Ваше утверждение немного неверно. 60fps — этакий стандарт для браузера. Соответственно, 60 вызовов функции перерисовки внутри браузера. Но суть не в этом. Я пока отброшу второй абзац вашего ответа, так как, если основываться на нем, то переменных (пользовательских свойств) CSS вообще быть не может, так как нет поддержки. В оригинальной статье автор обновлял DOM не только с целью проброски CSS переменных, но и с целью обновления данных для доступности, чего у вас уже нет. Для глаз это погоды не делает, а вот для слуха очень даже (по сему поводу чистая теория, так как нет желания проверить). Идем дальше. Ваш код создает отдельный слой и крутит его. И этот слой висит постоянно из-за анимации и не объединяется с другими слоями (http://prntscr.com/ir5mc8), посему вы не видите перерисовки. Такая «магия» дается не бесплатно, на нее выделяется память, что, как бы, тоже ресурс системы. Выше вам уже писали про «волшебное» свойство `will-change`, которое может сделать тоже самое для решения из оригинальной статьи. Добавьте `transition` и увидите теже 60FPS (появится постоянная перерисовка). Ну а если учесть
                Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать. Но все это можно поправить заново задав keyframe.
                то получается, что нам нужно точно так же пробрасывать в DOM кучу стилей не реже, чем раз в секунду. Так в чем профит-то? IE11?

                И про загадочные 50.2FPS все тоже просто и понятно. Если открыть счетчик FPS на оригинальных часах, то там будет 1FPS (логично же, 1 перерисовка всего, но, на самом-то деле, это тоже может варьироваться, так как процессор можно нагрузить так, что будет 0FPS). На вашем же скрине видно, что включена настройка отображения перерисовок (зеленый прямоугольник), которая сама по себе вызывает перерисовку, так как этот самый зеленый прямоугольник появляется / исчезает плавно. Вот и FPS проседает (а проседает ли? :)).
                  0
                  Соглашусь с вами. Коментарий ivan386 натолкнул меня на размышления, и я был совсем даже не прав в этой статье. И даже подумываю разгромить себя же в следующей. Скажу вам так — вы очень много где правы, но пользуясь только Chrome вы понемногу начинаете презирать другие браузеры. Chrome лучший не во всем, хотя и я сам в основном им пользуюсь.

                  Вы очень не правы тут:
                  Соответственно, 60 вызовов функции перерисовки внутри браузера.

                  И даже тут:
                  Так в чем профит-то? IE11?
                  Каждый браузер важен на столько, на сколько им пользуются именно ваши посетители ресурса.

                  И я был не прав тут:
                  Так как часы отсчитывают время CSS-ом, то если забить тред какими-то тяжёлыми заданиями — часы перестанут идти и когда страница освободится — будут отставать.
                  Хотя зависит от реализации keyframe в браузере. Но многим такие вещи не по чем.

                  Скажу вам больше, мы все очень много не знаем про браузеры. Лично меня комментарии к этой статье заставили усомниться в своем понимании роботы браузеров, и даже в том что нам про них (не)говорят разработчики.
                    –1
                    Мои доводы касались тех проблем, решение которых описывалось в статье. Речь шла о производительности, а не о поддержке IE. Если бы все описывалось в контексте обратной совместимости, то мне это было бы неинтересно, так как, благо, под IE уже давно не пишу.

                    Про то, как работают браузеры (хоть открытые, хоть закрытые), написано множество статей + море информации можно найти в исходниках (не у всех открыты), багтрекерах, инструментах разработчика и прочем.

                    60 вызовов — это к тому, что штуки вроде `requestAnimationFrame`, CSS animation / transisition.., как раз и ограничиваются этими 60-ю кадрами, то бишь 60-ю перерисовками. Если transform отправляет что-то на GPU, а инструменты разработчика Chrome'а не показывают перерисовки, то это вовсе не означает, что ничего не происходит. Картинка-то на экране меняется. Вот как раз с потолком в 60FPS и меняется. А Paint flashing (зеленый прямоугольник) этого и не покажет.
              0
              Здравствуйте, рекомендуймый FPS для страницы, не зависимо от контента — 60. И к сожалению, так с ходу, я не могу вам сказать почему в данном случае он 50, но факт остается факто — мы пишем в DOM с помощью JS каждую секунду и это нагружает главный поток. Кроме того наше переопределение CSS переменных вызывает перерасчет некоторых CSS аттрибутов, что нагружает главный поток опять.
              Интереса ради я просто попробовал изменять CSS переменные каждую секунду в пустой странице, и даже так у меня появлялись странный просадки FPS, хотя не так частые. Я не утверждаю что не стоит вообще не использовать CSS переменные, просто возможно не стоит только ради часов нагружать главный поток.

              На счет вашей функции сверху — она покажет FPS на основе только одного кадра, что не говорит ни о чем. Чтобы узнать точное FPS нужно считать среднее значение хотя бы 100 кадров. А так, это как говорить, что все люди выше 2 метров на основании роста Шакила О'Нила. Потому, простите, по Хром тут точно лучше обчистил FPS.
              0
              Да, это вполне очевидное решение, тоже вначале подумал об этом.
              Но, смутило как-раз то, о чём вы упомянули. Лаги могут появиться не только от загруженности страницы, но и от загрузки проца.
              И ещё мне не понравилось то, что атрибут datetime не будет меняться. Т.е. не факт, конечно, что это нужно. Но могут быть случаи, когда текстовая составляющая также важна, как и визуальная. Я имею в виду скринридеры.
              Если это не столь важно, то, конечно, такой способ приоритетней.
              Спасибо.
                0
                Я очень сомневаюсь что хоть одна читалка будет рада ежесекундному изменению времени. Я работал пару раз с читалками и для них достаточно важно не изменять контент без надобности. Да и представте себе — читалка еще не успела прочитать время, а вы уже его поменяли 5 раз.
                Я бы сказал, что для читалок лучше спрятать такой элемент (aria-hidden=«true») и создать скрытый элемент где мы меняем время раз в минуту.

                На счет нагрузок — да. Я не знаю как себя поведет CSS при нагрузках процессора, я поэксперементирую с этим и сообщу вам результат. Но, хорошо сделанная страница не должна нагружать поток на столько, чтобы часы сильно отставали (больше 5 секунд разницы в час), а такими малыми отклонениями можно пренебречь.
                Также, ради эксперимента оставлял вкладку с часами открытой, пока было запущено пару тяжелых процессов и еще 20 вкладок и часы за 2 часа так и не отстали.
                В случае сна и гибернации — ничего не гарантирую. Но можно переопределять CSS классы при возвращении пользователя на страницу, много проблем это не вызовет.

                Сама статья — это все лишь концепт, а не готовое решение.
                0
                Очень интересная статья, спасибо автору. Была подобная идея, но до реализации руки не дошли.
                  +2

                  Очень странно что не смотря на то что картинка очевидно меняется хром и firefox не подсвечивают перерисованную область. Но график fps в хроме при этом активно двигается показывая постоянную перерисовку.


                  В недавней статье "Этот SVG всегда показывает сегодняшнюю дату" в комментариях разместили часы в SVG. Они работают как секундомер на странице.


                  Анимация в SVG работает даже если вставить его в тег IMG. Но тогда начинает перерисовываться полностью всё изображение. И этот секундомер не хило нагружает процессор.


                  Я поэкспериментировал и сделал свой SVG секундомер в котором анимация включается каждую секунду на 0.001 секунды.
                  image


                  Думаю fps при этом маленький не потому что нагрузка большая а потому что рисовать нечего. Каждую секунду рисуется новый пик и график останавливается до следующего шага(если не включена подсветка прорисованной области).


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

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое