SVG в реальной жизни. Доклад Яндекса

Привет, меня зовут Артём, я руководитель одной из групп разработки интерфейсов в Яндексе. Неделю назад на Я.Субботнике я рассказал, как мы использовали SVG для создания внутреннего календаря. Это расшифровка моего доклада, несколько историй из реализации виджета календаря: масштабирование, заливка паттерном, маски, символы и особенности формата.

afhjb2fysygwu1kqxtxppwd181k.jpeg

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

Начали мы, конечно, с макета. Он выглядел так:

sv5uy0sn1jwdgsv06nnvxzs2nqu.jpeg

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

98xkb3ij0xigknbxr2cuh_ceazw.jpeg

Мы начали выбирать, на чем же мы будем его делать. Сделали несколько разных прототипов. Начали с canvas, но там надо было много кода, масштабирование вручную писать. У нас была идея, что календарик занимает столько места, сколько нужно, в разных лэйаутах он разной формы и разного размера. А для сanvas это было сложновато.

Был прикольный прототип, когда мы генерировали всю эту картинку линейными градиентами, но она при масштабировании и при переходе на ретину съезжала. Поэтому в итоге мы пришли к SVG. Почему? Во-первых, там полностью независимая от документа система координат, поэтому можно внутри расположить всё абсолютно, и это никак не сломается независимо ни от чего. Также там есть нормальная работа с масштабированием. Даже если в браузере сделать зум, если открыть на ретине или как угодно растягивать календарик, он будет ресайзиться как картинка и в любом случае выглядеть нормально. У нас на макете была заливка клеточками, и очень хорошо, что в SVG есть заливка паттернами.

2kfrgg1wo76nrglcb5porf-rx88.jpeg

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

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

vezulfk7ebhp9tbicgmjrahsagc.jpeg

Начнем с основы. SVG — гигантская координатная плоскость, на которой можно произвольно размещать векторную графику. При этом часть области, которую мы видим, определяет viewBox, а что за ее границами — это такой overflow hidden на стероидах. Что бы там ни было, его не будет видно. Мы решили, что для простоты расчетов сделаем в календаре один пиксель, равный одной минуте. Поэтому один час будет занимать ровно 60 пикселей. Чтобы было еще проще, мы решили, что день по ширине тоже будет 60 пикселей — чтобы все было квадратным, как в армии. И начали верстать.

Viewbox задается четырьмя параметрами. Первые два — верхняя левая точка в системе координат, от которой считается viewBox, для нас это 0,0. При этом ширина — это 60 * на количество дней, а высота — 60 * на количество часов.

Внутрь SVG валидно вставлять другие документы SVG, в которых внутри будет своя система координат. И чтобы события в дне можно было позиционировать только по вертикальной оси, мы решили, что на каждый день заведем отдельный SVG, и их просто сместим по горизонтали на 60 * на позицию дня в календаре. Тогда все события можно будет просто по вертикали по Y ставить, будет очень удобно. А внутрь каждой SVG, которая представляет собой день, мы положили прямоугольник, который будет отображать заливку дня.

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

81n0nmbffb0doxecq1gvx7wcc5s.jpeg

Заготовка есть. Теперь надо добавить сетку. Так как мы хотели ресайзить календарь, а линии сетки должны быть всегда однопиксельными, мы использовали атрибут vector-effect=non-scaling-stroke. Это приводит к тому, что как бы мы ни ресайзили, ни зумили, всегда будет однопиксельная сетка. Достаточно просто горизонтальных и вертикальных линий нужное количество добавить, и будет такая сетка.

ywokhgezdap7lon0dmcuisktia8.jpeg

С основой разобрались, перейдем к событиям на весь день. Это такая хитрая штука. Вы замечали, что в календарях есть события и есть галочка «на весь день». Эти события отличаются тем, что они идут весь день, независимо от того, в каком часовом поясе вы на них смотрите. Поэтому если где-нибудь в самом начале часовых поясов на Аляске событие начинается рано утром, то где-то через 48 часов в противоположном конце земного шара оно все еще будет идти. Звучит сложно, но для реализации это проще всего: просто сравниваешь дату с датой отображаемого дня. Если попадает — значит событие в этот день. Если два события на весь день попадают на день, то показывается то, которое позже началось. Так заливкой отображаются события на весь день.

7mrzwvwwi6clluc6jdh4gqkfyiu.jpeg

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

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

5gtrtkzwlyddpnutc-pifopjssq.jpeg

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

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

Нам надо сделать в этом паттерне клеточки. У нас 60 на 60 пикселей, клеточки должны быть 6 на 6, поэтому мы объявили паттерн 12 на 12, и внутри него нарисуем , как на схеме слева. У него есть атрибут d, который обозначает, как именно двигается линия. Он начинается из точки 0,0, и потом по координатам стрелками показано, как именно он рисуется. Если мы зальем его белым, получится такой узор: что не залито белым, залито черным.

vgswfu88zuzfiawxscejq7ral5o.jpeg

Переходим к следующему шагу, теперь объявим маску.  — это такой элемент в SVG, который позволяет добавлять другим элементам альфа-канал. То, что в маске нарисовано черным, в том элементе, к которому маска применена, невидимо, прозрачно. То, что нарисовано белым, непрозрачно. То, что серым, то полупрозрачно. У нас черно-белый паттерн, и мы внутрь маски добавим прямоугольник, и его этим паттерном зальем. Теперь у нас есть маска.

Следующий шаг — . Это такой тег в SVG, который позволяет объявлять переиспользуемую графику. Чаще всего символы используются, например, для иконок. И здесь мы объявим символ, внутрь которого положим два прямоугольника. Один ничем не зальем, чтобы он наследовал свойство fill от родительского SVG, а другой зальем currentСolor и применим к нему маску. Теперь у нас будет два прямоугольника: один с дырками и залит currentColor, а другой без дырок и залит fill. И они друг на друге лежат. Если мы зададим эти цвета одинаковыми, у нас будет сплошная заливка. А если разными — клеточки. К этому всё и шло. Теперь можно просто использовать CSS и через классы задавать произвольную заливку двух цветов для всех событий.

ljpplaevfwydccmgwqm3zs0bhmq.jpeg

Теперь надо определить, какие именно события должны попасть в календарь в тот или иной день. У нас есть часовой пояс +3, в котором мы все сидим, в нем есть шкала от 9 до 20 часов. Также есть человек, который сидит в условном Оренбурге, у него часовой пояс +5, его шкала смещена относительно нас на два часа. Мы сделаем проекцию на UTC, и видим, что по UTC этот промежуток от верха до низа надо отобразить в дне, чтобы пользователь мог, переключаясь между часовыми поясами, видеть и события, которые попадают в его календарь, и календарь того, на кого он смотрит.

Запомним эти числа, которые лежат в offset, потому что проще всего события, которые приезжают в UTC, позиционировать в этом же самом UTC. Для этого мы возьмем тег , который обозначает в SVG группу, и все события там спозиционируем абсолютно по UTC, а сам будем смещать на нужное нам количество пикселей, чтобы отображался тот или иной часовой пояс.

0lyujkyfu18dmb5kpimya6fio2w.jpeg

Подытожив это исследование, мы получаем, что у нас есть символ, на который мы ссылаемся, есть тип события, уровень, четность, есть его -120 минут от начала дня в UTC и длительность 30 минут. Добавив все события, мы получим такую картинку.

ovbi6sas81myfk1z4f2s9xlxb-4.jpeg

Текущее время тоже делается просто, это будет линия с тем же эффектом non-scaling-stroke, чтобы она всегда была однопиксельной. Вот как она отображается.

Время на месте не стоит, и надо, чтобы стрелочка двигалась. Самый прикольный способ, который мы придумали, — анимация. Мы решили, что сделаем анимацию, которая будет смещать стрелочку на количество минут в сутках, и делать это за сутки. А чтобы она не постоянно медленно двигалась, а именно тикала раз в минуту, мы использовали steps (). И как только мы это добавили, время стало двигаться. При этом на самом деле, так как анимация не гарантирует, что будет постоянно двигаться, она то отстает, то еще что-то. Но у нас события в календаре время от времени обновляются, и где-то раз в две-три минуты или когда пользователь ушел со вкладки и вернулся, весь календарь перерисовывается и время обновляется. Поэтому анимация видна, только когда ты сидишь и пристально смотришь, тикает она или нет.

ui82pqjamllc_ee-1ng6d0ujbh4.jpeg

Есть одна проблема. Здесь я сделал календарик пошире, чтобы он больше был похож на тот, что в продакшене. Стало видно, что клеточки уже не квадратные. Это потому что пропорции не сохраняются, и если мы растягиваем или изменяем соотношения сторон физически, то изменяется оно как в картинке. Чтобы этого избежать, надо написать немного JS. Есть соотношение сторон viewBox, которое было в нашем изначальном SVG, и фактическое соотношение сторон, которое используется у нас в верстке. Если найти отношение этих соотношений и потом его засунуть в трасформ паттерна, то клеточки станут квадратными. А еще этот коэффициент, который мы тут получили, можно использовать, если мы хотим понять, куда кликнул пользователь. Так как у нас одна минута в исходном SVG равна одному пикселю, то по координатам клика, умноженного на этот коэффициент, можно понять, в какое время попал пользователь.

e2ozghfj0zoqbzcfz57anw9ycba.jpeg

Осталось добавить HTML, чтобы были буквы и цифры сверху. Получится календарик.

puxho9vkcjuawtkaa9mpf0kadzi.jpegsqblcctralj1csyh_putsqfszh4.jpeg

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

jtbzx9epd4it3m5fbys7yf0waew.jpeg

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

Пользуйтесь CSS, пользуйтесь SVG. Спасибо!

© Habrahabr.ru