Пример — часы на СSS3 без изображений и JavaScript

:

Это статья о том, как был сделан демонстрационный пример Wall Clock in Pure CSS3. Изначально, я предполагал написать данную статью в виде пошагового учебного пособия, но в процессе я понял, что такой пример мало для этого подходит. Потому я решил остановиться только на деталях, которые, на мой взгляд, наиболее интересны, а реализацию остального можно посмотреть и в коде.

Для начала собственно сам пример — на codepen.io или на cssdesk.сom

Про мое знакомство с HTML


Я познакомился с HTML в 1999 году и с тех пор от случая к случаю я занимался frontend-разработкой для веб, как правило, скорее как хобби, и очень изредка, как часть какого-нибудь большого рабочего проекта. С 2012 года я ушел работать в freelance, а frontend-разработка для Web стала одним из наиболее востребованных навыков.

Часы


И так совпало, что копаясь в новогодние праздники в стопке старых CD и нашел там эту самую первую свою страницу, на которой я изучал HTML и позволившую в 1999 году мне автоматом получить зачет в институте. На ней был вставлен на Java апплет с часами.

А я как раз закончил небольшой проект с интенсивным использованием CSS3, и, идея сделать часы, но уже на чистом CSS3 лежала на поверхности. Я понимал, что такая идея для целей демонстрации модулей CSS3 Animation и CSS3 Transformation не будет новой, и решил сделать их хотя бы реалистично. Плюс я заранее решил не смотреть, как это реализовал кто-то еще. Ну а насколько у меня это получилось судить вам.

IKEA


Периодически бывая в IKEA, я часто обращал внимание на часы IKEA PUGG (картинка справа) из-за завораживающего шрифта цифр. Именно поэтому сначала в качестве макета были выбраны именно эти часы, но потом я нашел подобные часы у другого производителя и у них уже не было дурацкой ступенчатой окантовки корпуса.

Выбор размера и масштабирование c помощью font-size


Я сразу решил, что не будет привязан к пикселям, чтобы оставить возможность масштабирования.
Идеально бы подошли проценты, но была одна проблема. Поскольку бы решено не использовать графику, то цифры на циферблате нуждались в шрифте, а задать размер шрифта в процентах от размера блока не представлялось бы возможным.
Поэтому все версталось в em-единицах. Таким образом задать нужный размер часов, можно задав размер шрифта для блока. Изначально часы верстались размером в 100em x 100em и масштабировались через установку font-size.
Позже, из-за багов масштабирования и для целей демонстрации на codepen.io, я уменьшил “внутренний масштаб” до 10em.

Магия box-shadow


Для эффекта хромированной рамки понадобились три вложенных контейнера: первый для внешней части рамки, второй для внутренней части рамки, а третий представлял собой основание циферблата.

Сам по себе эффект быть создан за счет элементов теней в box-shadow для всех трех контейнеров. Использовалась как внешние, так и внутренние типы теней. Помочь понять принцип наложения теней, а также порядок и приоритет наложения поможет следующий псевдокод и картинка:

<div id="a"><div id="b"><div id="c">...</div></div></div>
#a { box-shadow:
		inset внутр.тень ai1, inset внутр.тень ai2, внешняя тень ao1, внешняя тень ao2;   }
#b { box-shadow:
		inset внутр.тень bi1, inset внутр.тень bi2, внешняя тень bo1, внешняя тень bo2;  }
#c { box-shadow:
		inset внутр.тень ci1, inset внутр.тень ci2, внешняя тень co1, внешняя тень co2; }

Чтобы добиться хотя бы небольшого правдоподобия хромирования пришлось изучить не один десяток фотографий, и не один час времени. Сам результат в CSS приводить я здесь не буду, желающие могут поэкспериментировать с получившимся кодом например на codepen.io

Размещаем метки


Дабы не плодить много сущностей и довольствоваться минимумом CSS правил была придумана следующая HTML разметка блока из пяти меток
<b><i><i><i><i></i></i></i></i></b>

Эти пять тэгов представляли собой метки минут с 0 по 4 и одновременно с 30 по 34. Вложенность тегов позволяла повернуть их относительно своей оси всего одним правилом в CSS
b > i, i > i  { transform:rotate(6deg) ; }

Сами метки рисовались при помощи верхнего (0-4) и нижнего (30-34) бордюра. C учетом оформления, мы получали из этих пяти тэгов следующий результат на выходе:

Оставшиеся пять блоков меток были размещены схожим методом путем размещения друг в друге:

<b><i>…<b><i>…<b><i>…<b><i>…<b><i>…<b><i>…</b></b></b></b></b></b>

Соответственно следующее правило размещало оставшиеся блоки по нужным местам
b>b { transform: rotate(30deg) ; }

Но на удивление такой простой код в условиях моего примера «поставил в затруднительное положение» большинство браузеров:

На удивление, правильный результат был показан только в IE9. В IE10 баги уже нашлись.

Firefox и браузеры на Webkit обнаружили в себе фундаментальный баг, связанный с округлением промежуточных значений координат при «вложенных» трансформациях. Из-за этого бага, как вы видите, происходит накапливание ошибки, и последние метки размещаются достаточно далеко от необходимого места.

Opera же вообще отказалась показывать трансформации правильно. Это конечно здорово, что Opera прошла ACID3, но на качестве другого кода, как мы видим это не отражается. И позже, я не был удивлен сворачиванием разработки Prestо, хотя, честно говоря, был расстроен.

Для того, чтобы обойти баги браузеров, пришлось использовать более топорную реализацию рисования меток. При этом я сохранил возможность переключения между версиями в своем playground-e для демонстрации рабочей версии с одной стороны и обнаруженных проблем с другой.

Цифры


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

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

Для превращения изображения в собственно шрифт, было найдено хорошее руководство «How To Make Your Own Symbol Font», предлагающее для этого использовать следующую цепочку цепочку ПО:

[Изображение] -> Inkscape -> [SVG шрифт] -> freefontconverter.com -> [TTF шрифт] -> fontsquirrel.com/fontface/generator -> [Web-набор шрифтов]

Шрифт получился достаточно красивый, но с одной проблемой – из-за особенностей InkScape все буквы были одной ширины. Проблема решилась путем редактирования TTF шрифта с помощью бесплатного редактора шрифтов Type 3.2 Light.

Позже Chrome преподнес мне неожиданный сюрприз и к вопросу со шрифтом пришлось вернуться. Но об этом ниже.

Стрелки


Никаких сложностей при реализации часовой и минутной стрелки не наблюдалось, а для движения стрелок по циферблату в соответсвии со временем, как изначально планировалось, использовалась CSS3 анимация поворота стрелки периодом 12*60*60 сек и 60*60 сек, соответсвенно.

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

Замена свойства animation-function c linear на steps(60) была ближе к тому, чего хотелось, но со временем я стал замечать, что телепортация секундной стрелки с одной метки на другую выглядит неестественно.

Простейшим вариантом реализовать близкое к реальному движение секундной стрелки было бы расширение блока @keyframes отвечающего за анимацию, где потребовалось бы описать остановку стрелки на каждой из секундных меток.

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

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

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

.container {
	animation: tick 1s normal infinite linear;
 }
@keyframes tick {
	0% { transform: rotate(0deg); }
	8.3% { transform:rotate(5.5deg); }
	100% { transform: rotate(0deg); }		
}
.arrow {
	animation: a360 60s normal infinite linear; 
}
@keyframes a360  {
	0% { transform: rotate(0deg) }
	100% { transform: rotate(360deg) }
}

UPDATE:
Всвязи с жалобами на перформанс казалось бы простейшего движения стрелок, был проведен небольшой reseach и все линейные функции анимации были заменены аппроксимацией. Например, в коде анимации минутной стрелки теперь вместо linear можно красуется steps(3600) .

Тени


Реализовав стрелки я немедленно добавил тень для них с использованием box-shadow и прозрачности. Кроме неприятного бага с тенью при ступенчатом движении секундной стрелки, обнаружилось следующая неприятная картина наложения теней в центре.

Поэтому box-shadow было удалена, а тень реализована путем создания копии стрелок с заливкой цвета тени и размещенной под цифрами и метками. Для этого пришлось добавить несколько тэгов и пару CSS правил.

Показываем актуальное время


Конечно часы на то и часы, а не секундомер, и должны показывать актуальное время. Это можно было бы сделать при помощи пары строчек на JavaScript, но это было бы не кошерно. Поэтому установкой времени занимается серверный скрипт, генерирующий CSS с предустановленными значениями углов поворота стрелок на момент запроса. И да, используется Greenwich Meridian Time.

Playground


Получившийся CSS3 без фиксов получился достаточно небольшим – чуть более 200 правил, и никакого copy-paste кода. HTML код тоже получился достаточно компактен и соответствует парадигме: одна сущность – один тэг.

Но поняв, что результат, работающий во всех браузерах значительно отличается от чистого СSS3, я сделал небольшую страницу-тест, где кроме собственно часов можно посмотреть и оценить насколько хорошо браузер справляется с версией без фиксов.

Собственно playground @github-pages

Совместимость и баааааги.


Часы разрабатывались в Firefox (no prefixes!), и, по мере разработки, проверялись в последних версиях всех достаточно популярных браузерах. Но признаю, что качественный результат меня удивил. Лучше всех походу разработки справлялся с реализацией стандартов IE9(sic!), очень жаль что отсутствие поддержки CSS анимации вывело его из игры.

Что касается остальных браузеров, то по-видимому, скорость внедрения новой СSS функциональности значительно сказалась на качестве результата (в плохую сторону), что отразилось даже на IE10.

Один общий для нескольких браузеров баг (будем называть мета-багом) с расчетом трансформаций я описал выше. Единственное, стоит добавить, что в результате использования встроенного в эти браузеры масштабирования, мы получаем еще более значительные артефакты.

Второй «мета-баг», который оказался общим для всех браузеров кроме IE – это нарушение анимации стрелок при изменении размера часов. Баг отлично воспроизводится, когда одновременно с анимацией стрелок мы изменим размер часов с участием CSS3 transition.

Webkit (Chrome)


После похорон Presto, можно констатировать, что из трех оставшихся Webkit – как самый богатый на СSS3 функциональность движок, одновременно является наиболее глючным по результатам этого примера.
Бага с размером

Если вы уже открыли покопались в playgroundе с помощью Chrome, то увидели ее сразу.
При некоторых задаваемых размерах шрифта, в Webkit отчего-то нарушается расчет em размеров, и часы отказываются уменьшаться.
Сюрприз со шрифтом

Когда часы были почти закончены, и я стал готовиться к public презентации, я неожиданно заметил, что в Chrome цифры циферблата потеряли antialiasing.

Путем манипуляций было выяснено, что виной тому анимация стрелок в границах текста цифр. Долгое плутание по интернету ничего не дало, кроме кучи бесполезных специфических CSS свойств, которые должны были управлять сглаживанием. То есть на лицо просто баг Сhrome под Windows.

К счастью я наткнулся на интересную статью касающуюся библиотек, используемых Chrome для рендеринга шрифтов под Windows. Это дало мне идею попробовать сконвертировать Truetype шрифт в PostScript шрифт – и ура! сглаживание вновь появилось.

Что касается PostScript шрифта, то конвертация TrueType-кривых в PostScript-кривые была сделана при помощи редактора шрифтов Type3.2 Light и для возможности подключения на страницу шрифт был сохранен в OpenType формате.

По ходу пьесы также выяснилось, что Chrome под Windows, используя те же системные библиотеки что например и IE, не обратил внимание на заданную в OpenType таблицу кернинга, что не выгодно отличает его от Firefox.

Итоги


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

На мой взгляд, что получилость, с точки зрения CSS3:
• Ступенчатое дижение секундной стрелки
• Псевдо-хромированный обод часов
• Инскементивное размещение меток с помощью transform: rotate
• Возможность изменять размер часов путем установки font-size для контенера.
• Набольшой размер CSS файла (без учета кросс-броузерной совместимости)
и отсутсвие повторяющихся кусков в CSS коде
• Небольшой HTML, одна сущность — один тег

Ссылки


Демонстрация @codepen.io
Демонстрация @cssdesk.сom
Playground @ github pages
Проект github

Одни из первых реализаций часов на CSS, которые я нашел, когда закончил разработку своего примера:
Paul Hayes в 2009
Zoltan Hawryluk в 2010
Lennart Schoors в 2010

Ну и конечно жду комментариев и готов ответить на вопросы.

UPDATE: Перформанс примера несколько улучшен, описание выше по тексту.

UPDATE 2: Пример потихоньку расползается по интернет
и порой всплывают интересные твиты, например воттакой: