Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть