Как мы писали SVG виджеты для JavaScript

в 10:16, , рубрики: gauges, javascript, svg, widgets, Веб-разработка, я пиарюсь, метки: , , ,

Дисклеймер.

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

Поэтому, я попытаюсь сделать так, чтобы эта статья была полезна не только автору, но и читателям. Я опишу не столько что мы делали, сколько как мы это делали. Начнём мы, естественно, с задачи, которая перед нами стояла.

Задача

Вот такие виджеты можно сделать
Представьте себе, что вы – web-программист, который реализует сложную SCADA систему, дашбоард (простите, но внятного перевода этого слова на русский я так и не встретил), интерактивную систему управления метриками, или просто вам нужно вставить на ваш сайт часы с хитрым дизайном. При этом вам нужно добавлять туда всяческие шкалы, крутилки со стрелками (на английском это называется Gauge), часики и другие «приборы», возможно даже интерактивные.

С первого взгляда, эта задача решается довольно просто. Например, есть бесплатный компонент Google Gauge и множество различных штук, которые выпадают по запросу в том же Google. С другой стороны, в большинстве таких библиотек набор вариантов, как правило, ограничен. Как только вам надо сделать что-то своё – начинает работать принцип «проще написать самому».

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

Итак, команда в составе двух программистов, QA (который подключился чуть позже) и менеджера продукта (который постоянно всем мешал работать, заставляя всё переделывать, «потому что пользователю неудобно или сложно»), начала разработку сразу после новогодних праздников.

Дело облегчалось тем, что к моменту начала работы над проектом, у нас уже был аналогичный продукт для Windows Forms. Мы успешно утащили оттуда объектную модель виджета и редактор виджета для Windows.

Перед нами стояло несколько технических задач, которые предстояло разрешить:

  • Кроссплатформенность. Виджет должен был работать на чистом HTML+JavaScript, независимо от платформы и технологии.
  • Стандарты. Виджет должен поддерживать CSS для кастомизации параметров.
  • Производительность. Во время смены значений стрелка должна двигаться плавно, не тормозить и не грузить процессор на 100%.
  • Интерактивность. Пользователь должен иметь возможность устанавливать значение, потянув за стрелку (при этом виджет, опять же, не должен тормозить).
  • Простота. Программисту должно быть просто и комфортно: встраивание виджета в Web-страницу должно происходить с использованием нескольких очевидных строчек кода.

Примерно так выглядит JavaScript код для работающих часов

Фреймворк

Прежде чем приступить к работе, нам нужно было выбрать инструменты. Рискну вызвать холивар и возмущённые вопли о том, что мы «не умеем готовить JS», но всё-таки скажу. Писать на «чистом» JavaScript – задача, требующая чрезмерной усидчивости, внимательности и слишком большого объёма «оперативной памяти» у программиста. По опыту заказной разработки могу аргументированно сказать, что качественная разработка RIA приложения на чистом JavaScript занимает в полтора-два раза больше времени, чем создание аналогичной по качеству системы с помощью Flash или Silverlight (да покоится он с миром).

В нашем случае дело усугублялось тем фактом, что у нас была профессиональная Silverlight-команда, которую предстояло переучить (увы, стратеги из Microsoft иногда умеет насолить собственным разработчикам). Соответственно, чтобы дойти до того же уровня в работе с «чистым» JS, потребовалось бы несколько лет и пара «слитых» продуктов.

Впрочем, мы нашли очень неплохой выход из ситуации, который, кстати, активно порекомендуем всем .NET разработчикам, пишущим RIA на JavaScript. Таким выходом стал фрэймворк под названием Script#, который разрабатывается сотрудниками Microsoft. Он позволяет писать код на C# и транслировать его в JavaScript. Причём, скажу абсолютно честно: качество JS-кода на выходе получается очень и очень приличное. Конечно, набор функций там достаточно ограничен, но спасает возможность реализовать отдельные методы целиком на JS, после чего они без изменений транслируются в результирующий файл. В общем, если вы пишете на C#, рекомендую попробовать.

Архитектура

SVG vs. Canvas

Виджет для отображения погодыСразу было понятно, что разработку надо вести, используя HTML5. Первым вопросом, которым мы, конечно, задались, был выбор между SVG и Canvas.

С одной стороны SVG как нельзя лучше подходил для работы с виджетами, собранными из отдельных объектов и примитивов в редакторе. С другой стороны, мы опасались за производительность SVG и его поддержку в разных браузерах. Очень не хотелось оказаться в ситуации, когда компонент нормально работает только в последней версии Google Chrome. Тем более что важным моментом была поддержка мобильных устройств: iOS, Android и прочего зоопарка.

Если смотреть на Canvas, то несомненным плюсом была аппаратная поддержка во многих браузерах, а очевидным минусом – необходимость сложной перерисовки объектов при анимации (например, при движении стрелки придётся перерисовывать часть объектов, находящихся под ней).

После некоторого размышления мы поняли, что вся система отрисовки должна писаться в абстракциях, не зависящих от специфики SVG или Canvas (привет, Кэп). После этого, реализуется отдельный набор классов для отрисовки и перерисовки объектов с использованием конкретной технологии. В первой версии мы решили ограничиться SVG, поскольку нас сильно привлекала возможность крутить стрелки с помощью атрибута angle, а не перерисовывать картинку на каждый поворот. Кроме того, мы хотели использовать стандартную SVG-анимацию (увы, с этим возникли большие проблемы) и CSS для управления стилями виджета.

Таким образом, пока все виджеты рисуются на чистом SVG.

Объектная модель

Примерно так выглядит структура виджетаС учётом того, что у нас уже была готовая система для Windows Forms, из которой как минимум редактор мы собирались активно использовать, решено было взять за основу ту же самую объектную модель и немного адаптировать её под Web.

С учётом того, что разработка велась на Script#, портирование модели свелось к банальной «копипасте». Все попытки оставить одну базу кода, к сожалению, успехом не увенчались. Слишком много было различий в поведении, отрисовке и прочих мелочах. Из-за этого появлялась куча условных деректив и ветвлений, которые лишь затрудняли понимание кода и приносили больше вреда, чем пользы. В результате было решено, что принцип DRY не обидится. Как показало время, мы всё сделали правильно – в коде веб-части практически не осталось кусков, идентичных Windows части.

Впрочем, чтобы быть объективным, есть и успешные примеры разделения кода между C# и Script# продуктами.

Сериализация

Эти шарики на пружинках тоже двигаютсяДля хранения WinForms виджетов, которые служили отправной точкой нашей работы, мы использовали свой собственный бинарный формат. Файлы в этом формате читались и сохранялись редактором виджетов. С другой стороны, очень хотелось, чтобы в Web-версии виджет задавался «родным» для JavaScript способом. Очевидно, что для этого мы выбрали JSON.

Изначально планировалось, что JSON с описанием виджета будет создаваться пользователем (т.е. программистом, использующим наш продукт), но потом от этой идеи немного отошли в сторону. JSON с описанием объекта создаётся при экспорте виджета в редакторе. В первой версии мы пошли путём наименьшего сопротивления и прикрутили стандартный JSON-Serializer, который поставляется с .NET Framework 3.5. Не вдаваясь в детали реализации, скажу, что объектная модель виджета стандартным образом сериализуется в JSON и результат отдаётся программисту. Программист при добавлении виджета на страницу передаёт этот JSON как параметр конструктора (в JavaScript). При создании виджета, JSON разбирается движком, и по нему строится набор SVG-примитивов.

Одним из самых основных недостатков, борьба с которым уже запланирована на ближайшее время – обилие лишней ненужной информации в результирующем JSON-описании. Это происходит из-за того, что мы используем стандартные механизмы сериализации и получаем копию объекта «as is», включая кучу ненужных свойств (приходится, увы, сериализовать все public поля). Из-за этого JSON-описание выглядит монструозным и занимает довольно много места. Мы даже специально сделали JSON-инспектор, чтобы программисты могли посмотреть, как их виджет устроен, загрузив JSON с описанием в специальную форму.

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

Реализация

Анимация

Мы взяли типы и описание анимации из jQueryПонятно, что стрелка в шкале не должна (в большинстве случаев) перемещаться с одного значения на другое «квантовым скачком», а должна плавно проходить все промежуточные значения и, немного зайдя за нужное значение, элегантно туда возвращаться. К тому же, важно, чтобы виджет не съедал 146% процессора, выполняя это плавное движение.

Вообще, изначально мы пытались по-честному использовать CSS-анимацию… Увы, мы потерпели сокрушительное поражение, потеряв при этом две недели. Это, наверное, наиболее яркий момент, где в полную силу проявились особенности веб разработки, порождаемые зоопарком из браузеров, которые не хотят работать с одним стандартом.
В результате, после того, как стало окончательно ясно, что quod licet Chrome non licet Internet Explorer, мы написали на JavaScript собственную анимацию с преферансом и куртизанками. Это сильно развязало нам руки. Например, мы смогли реализовать практически все варианты ускорения-замедления и прочей красоты.

Основные опасения, что всё будет тормозить, оказались напрасными и, как только, мы перестали перерисовывать весь виджет на маленькое изменение позиции стрелочки, всё стало просто летать. Вывод – нормально оптимизированная перерисовка «вручную» по SetInterval практически не уступает в производительности CSS-анимации, но работает одинаково во всех браузерах.

CSS

Стрелки всех виджетов сконфигурированы с помощью CSS
Перед нами стояла задача – дать программисту возможность конфигурировать цвета отдельных элементов виджета в CSS. Вообще, никаких проблем тут нет. Есть SVG элемент, у него есть класс, есть CSS с настройками для этого класса. Единственное, что разочаровало – невозможность нормально задать в CSS градиент для SVG. По спецификации, чтобы использовать градиентную заливку для объектов SVG, необходимо её сначала декларативно описать в том же SVG.

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

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

Интерактивность

Эта автомобильная панель - один виджет, собранный из нескольких приборовМы принципиально писали виджеты так, чтобы пользователь мог, потянув за стрелку или ползунок, установить новое значение. Конечно же, виджет при этом вызывает callback и оповещает JavaScript.

Обработка mousedown и mouseup делалась для div’а, в который был вставлен виджет. При нажатии кнопки мыши, мы определяли, на какой элемент нажал пользователь и, соответственно, выстраивали дальнейшую логику в зависимости от этого.

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

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

В конце концов было решено писать HitText для каждого компонента отдельно, вспомнив аналитическую геометрию и аппроксимируя объект более простыми примитивами.

Ещё одна небольшая задача возникла, когда мы начали перерисовывать виджет в ответ на действия пользователя. Например, пользователь «хватает» стрелку и начинает крутить её с огромной скоростью. Естественно, при этом надо много чего перерисовать. Положительным моментом использования векторного SVG была возможность перерисовать (а в большинстве случаев – просто повернуть) только перемещаемые объекты и не трогать остальные, тогда как при использовании Canvas нам пришлось бы перерисовать ещё и объект под стрелкой. С другой стороны, иногда приходилось перерисовывать довольно много.

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

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

Для того, чтобы реализовать поддержку «жестов» для touch-устройств, необходимо обрабатывать совершенно другие события, например, touchstart, touchend, touchmove, и т.д. Поэтому эту поддержку пришлось писать отдельно.

Скрипты и внутренняя логика

Линейные виджеты. Высота треугольника справа изменяется вместе со значением шкалыSharpShooter Gauges славился тем, что поддерживал внутренние скрипты. Эти скрипты были ключевым моментом при разработке виджета, так как определяли привязку положения графических элементов к шкалам и направляющим.

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

Изначально у нас была идея полностью перевести внутренний скриптинг в синтаксис JavaScript. Но, подумав о наших старых пользователях, которые не поймут, если их WinForms виджеты вдруг перестанут работать в новой версии системы, мы решили оставить внутренний скриптинг как есть и транслировать скрипты в JS при экспорте. Впрочем, объём этой героической инициативы превысил все мыслимые и немыслимые сроки и изрядно затянул релиз.

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

Во-первых, проблема в неявном приведении объектов (например, даты) к строке. Дело в том, что в JavaScript и .NET дата приводится к строке по-разному, что добавляет изрядную долю несовместимости версиям для Windows и HTML5.

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

Отдельные мелкие проблемы

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

Отсутствие возможности посчитать размер текста. В SVG, как и во многих других графических системах, нет возможности заранее определить, какой размер займёт текст после отрисовки. Так как версия для WinForms чуть более чем полностью полагалась на эту функцию GDI, нам пришлось выдумывать хитрые схемы для её реализации. Сейчас весь текст отрисовывается два раза. Один раз – для того, чтобы узнать размеры, а второй раз уже с нормальным позиционированием.

Проблемы с RGBA на iOS устройствах. Ближе к середине разработки, тестируя виджеты на разных моделях устройств, мы заметили, что на iPhone 4 и iPad наша заливка была абсолютно чёрной. Конечно, виджеты из-за этого выглядели очень готично и забавно, но всё-таки совершенно не так, как надо. После небольших изысканий, мы поняли, что формат RGBA, который использовался для определения цвета, не поддерживается в Safari под iOS4+ (что удивительно, на iPhone 3G всё работало). В результате мы разделили RGBA значение на два отдельных (цвет и прозрачность) везде, где мы его использовали. Помогло.

Выводы

Я уже говорил, что хотел бы избежать уклона статьи в “marketing bullshit” и сделать её более-менее полезной с технической точки зрения. Поэтому приведу несколько выводов, которые могут помочь при работе с SVG в JavaScript.

Вывод 1. Большинство современных браузеров поддерживают нормальную работу с SVG и дают визуально одинаковый результат.
Вывод 2. Фрэймворк Script# отлично справляется со своей задачей, позволяя использовать C# для разработки RIA для JavaScript и HTML5.
Вывод 3. Стандартная CSS-анимация пока слишком сырая и браузерно-зависимая, чтобы использовать её для работы. «Ручная» анимация, построенная на перерисовке части SVG работает везде одинаково и, при минимальной оптимизации, совсем не тормозит (даже в IE).
Вывод 4. Из CSS можно нормально конфигурировать стиль SVG элементов. Для того чтобы сконфигурировать градиентную заливку, её предварительно надо декларировать в том же SVG, поэтому одним CSS не обойтись. Коническая заливка, увы, не поддерживается.
Вывод 5. iOS устройства (за исключением самых старых) не поддерживают цвет в формате RGBA. Поэтому при задании стилей нужно задавать RGB и Alpha отдельно.
Вывод последний. Мы сделали классный продукт, который нравится нам самим :).
Ну и, конечно, посмотреть, что у нас вышло вы можете здесь.

Автор: Tomcat


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js