Воссоздание кнопки Arduino с использованием SVG и <lit-element>

Опубликовано: 2022-03-10
Краткое резюме ↬ HTML поставляется с набором элементов управления вводом, и существует множество библиотек компонентов, которые включают в себя множество стандартных элементов управления, таких как флажки и переключатели. Но что делать, когда нужно что-то необычное?

В этой статье вы узнаете, как создавать собственные HTML-компоненты, имитирующие физические объекты, такие как кнопка Arduino. Мы нарисуем компонент в Inkscape с нуля, оптимизируем сгенерированный SVG-код для Интернета и завернем его как автономный веб-компонент с помощью облегченной библиотеки lit-element , уделив особое внимание доступности и удобству использования на мобильных устройствах.

Сегодня я собираюсь провести вас через процесс создания HTML-компонента, который имитирует мгновенный компонент кнопки, который обычно используется с Arduino и в электронных проектах. Мы будем использовать такие технологии, как SVG, веб-компоненты и lit-element , и узнаем, как сделать кнопку доступной с помощью некоторых трюков JavaScript-CSS.

Давайте начнем!

От Arduino к HTML: потребность в кнопочном компоненте

Прежде чем мы отправимся в путешествие, давайте рассмотрим, что мы собираемся создать и, что более важно, зачем. Я создаю симулятор Arduino с открытым исходным кодом на JavaScript под названием avr8js. Этот симулятор может выполнять код Arduino, и я буду использовать его в серии учебных пособий и курсов, которые учат производителей программировать для Arduino.

Сам симулятор заботится только о выполнении программы — он выполняет инструкцию кода за инструкцией и обновляет свое внутреннее состояние и буфер памяти в соответствии с логикой программы. Чтобы взаимодействовать с программой Arduino, вам необходимо создать виртуальные электронные компоненты, которые могут отправлять входные данные в симулятор или реагировать на его выходные данные.

Запуск симулятора в одиночку очень похож на изолированный запуск JavaScript. Вы не сможете по-настоящему взаимодействовать с пользователем, если вы также не создадите некоторые HTML-элементы и не подключите их к коду JavaScript через DOM.

Таким образом, в дополнение к симулятору процессора я также работаю над библиотекой компонентов HTML, имитирующих физическое оборудование, начиная с первых двух компонентов, которые вы найдете практически в любом проекте электроники: светодиод и кнопка.

Светодиод и элементы кнопки в действии
Светодиод и элементы кнопки в действии

Светодиод относительно прост, так как имеет только два выходных состояния: включено и выключено. За кулисами он использует фильтр SVG для создания эффекта освещения.

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

Еще после прыжка! Продолжить чтение ниже ↓

Определение требований к кнопке

Наш компонент будет напоминать кнопку диаметром 12 мм. Эти кнопки очень часто встречаются в стартовых наборах электроники и поставляются с колпачками разных цветов, как вы можете видеть на фотографии ниже:

Simon Game с желтыми, красными, синими и зелеными кнопками
Simon Game с желтыми, красными, синими и зелеными кнопками (большой превью)

С точки зрения поведения кнопка должна иметь два состояния: нажата и отпущена. Они аналогичны HTML-событиям mousedown/mouseup, но мы должны убедиться, что кнопки также могут использоваться с мобильных устройств и доступны для пользователей без мыши.

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

Если вы хотите узнать больше, ознакомьтесь с докладом, который я провел с Бенджамином Грюнбаумом на SmashingConf Freiburg в 2019 году: «Анатомия клика».

Подводя итог нашим требованиям, наша кнопка должна:

  1. похожи на физическую кнопку 12 мм;
  2. иметь два различных состояния: нажатое и отпущенное, и они должны быть визуально различимы;
  3. поддерживать взаимодействие с мышью, мобильными устройствами и быть доступным для пользователей клавиатуры;
  4. поддерживать разные цвета крышек (по крайней мере, красный, зеленый, синий, желтый, белый и черный).

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

SVG для победы

Большинство веб-компонентов реализованы с использованием комбинации CSS и HTML. Когда нам нужна более сложная графика, мы обычно используем растровые изображения в формате JPG или PNG (или GIF, если вы чувствуете ностальгию).

Однако в нашем случае мы будем использовать другой подход: графику SVG. SVG гораздо легче поддается сложной графике, чем CSS (да, я знаю, вы можете создавать интересные вещи с помощью CSS, но это не значит, что так и должно быть). Но не волнуйтесь, мы не отказываемся полностью от CSS. Это поможет нам стилизовать кнопки и, в конечном итоге, даже сделать их доступными.

У SVG есть еще одно большое преимущество по сравнению с растровыми графическими изображениями: им очень легко манипулировать из JavaScript, и его можно стилизовать с помощью CSS. Это означает, что мы можем предоставить одно изображение для кнопки и использовать JavaScript для настройки цветовой шапки, а также стили CSS для указания состояния кнопки. Аккуратно, не так ли?

Наконец, SVG — это просто XML-документ, который можно редактировать с помощью текстовых редакторов и встраивать непосредственно в HTML, что делает его идеальным решением для создания многократно используемых компонентов HTML. Готовы нарисовать нашу кнопку?

Рисование кнопки в Inkscape

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

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

  1. Темно-серый прямоугольник размером 12×12 мм для пластикового корпуса со слегка закругленными углами для большей мягкости.
  2. Меньший светло-серый прямоугольник размером 10,5 × 10,5 для металлической крышки.
  3. Четыре более темных круга, по одному в каждом углу для штифтов, удерживающих кнопку вместе.
  4. Большой круг посередине, это контур пуговицы.
  5. Меньший круг посередине для верхней части пуговицы.
  6. Четыре светло-серых прямоугольника в форме буквы «Т» для металлических выводов кнопки.

И результат, немного увеличенный:

Наш нарисованный от руки эскиз кнопки
Наш нарисованный от руки эскиз кнопки (большой превью)

В качестве последнего штриха мы добавим магический градиент SVG к контуру кнопки, чтобы придать ей ощущение трехмерности:

Добавление градиентной заливки для создания эффекта 3D.
Добавление градиентной заливки для создания эффекта 3D (большой предварительный просмотр)

Ну вот! У нас есть визуальные эффекты, теперь нам нужно разместить их в Интернете.

От Inkscape к веб-SVG

Как я упоминал выше, SVG довольно просто встроить в HTML — вы можете просто вставить содержимое файла SVG в свой HTML-документ, открыть его в браузере, и он будет отображаться на вашем экране. Вы можете увидеть это в действии в следующем примере CodePen:

См. кнопку Pen SVG в HTML от @Uri Shaked

См. кнопку Pen SVG в HTML от @Uri Shaked

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

К счастью, Inkscape может убрать за нас большую часть беспорядка. Вот как это сделать:

  1. Перейдите в меню « Файл» и нажмите « Очистить документ» . (Это удалит неиспользуемые определения из вашего документа.)
  2. Снова перейдите в « Файл» и нажмите « Сохранить как… ». При сохранении выберите Оптимизированный SVG ( *.svg ) в раскрывающемся списке Тип файла.
  3. Вы увидите диалоговое окно «Оптимизированный вывод SVG» с тремя вкладками. Отметьте все параметры, кроме «Сохранить данные редактора», «Сохранить неиспользуемые определения» и «Сохранить идентификаторы, созданные вручную…».
(Большой превью)

Удаление всего этого создаст SVG-файл меньшего размера, с которым будет легче работать. В моем случае размер файла уменьшился с 4593 байт до 2080 байт, что составляет менее половины размера. Для более сложных SVG-файлов это может значительно сэкономить трафик и заметно сократить время загрузки вашей веб-страницы.

Оптимизированный SVG также намного легче читать и понимать. В следующем отрывке вы сможете легко найти два прямоугольника, которые составляют тело кнопки:

 <rect width="12" height="12" rx=".44" ry=".44" fill="#464646" stroke-width="1.0003"/> <rect x=".75" y=".75" width="10.5" height="10.5" rx=".211" ry=".211" fill="#eaeaea"/> <g fill="#1b1b1b"> <circle cx="1.767" cy="1.7916" r=".37"/> <circle cx="10.161" cy="1.7916" r=".37"/> <circle cx="10.161" cy="10.197" r=".37"/> <circle cx="1.767" cy="10.197" r=".37"/> </g> <circle cx="6" cy="6" r="3.822" fill="url(#a)"/> <circle cx="6" cy="6" r="2.9" fill="#ff2a2a" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08"/>

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

В общем, ручной проход по сгенерированному файлу SVG всегда полезен. Во многих случаях можно удалить пустые группы или применить матричные преобразования, а также упростить координаты градиента, сопоставив их с «пользовательского пространства при использовании» (глобальные координаты) на «ограничивающий прямоугольник объекта» (относительно объекта). Эти оптимизации необязательны, но вы получаете код, который легче понять и поддерживать.

С этого момента мы отложим Inkscape и будем работать с текстовым представлением изображения SVG.

Создание многократно используемого веб-компонента

На данный момент мы получили графику для нашей кнопки, готовую к вставке в наш симулятор. Мы можем легко настроить цвет кнопки, изменив атрибут fill меньшего круга и начальный цвет градиента большего круга.

Наша следующая цель — превратить нашу кнопку в повторно используемый веб-компонент, который можно настраивать, передавая атрибут color и реагирующий на взаимодействие с пользователем (события нажатия/отпускания). Мы будем использовать lit-element , небольшую библиотеку, упрощающую создание веб-компонентов.

lit-element отлично подходит для создания небольших автономных библиотек компонентов. Он построен на основе стандарта веб-компонентов, который позволяет использовать эти компоненты любым веб-приложением, независимо от используемой среды: Angular, React, Vue или Vanilla JS смогут использовать наш компонент.

Создание компонентов в lit-element выполняется с использованием синтаксиса на основе классов с методом render() , который возвращает код HTML для элемента. Немного похоже на React, если вы с ним знакомы. Однако, в отличие от реакции, lit-element использует стандартные теговые литералы шаблонов Javascript для определения содержимого компонента.

Вот как можно создать простой компонент hello-world :

 import { customElement, html, LitElement } from 'lit-element'; @customElement('hello-world') export class HelloWorldElement extends LitElement { render() { return html` <h1> Hello, World! </h1> `; } }

Затем этот компонент можно использовать в любом месте вашего HTML-кода, просто написав <hello-world></hello-world> .

Примечание . На самом деле, наша кнопка требует немного больше кода: нам нужно объявить свойство ввода для цвета, используя декоратор @property() (и со значением по умолчанию красного цвета), и вставить код SVG в наш render() , заменяя цвет шапки кнопки значением свойства color (см. пример). Важные биты находятся в строке 5, где мы определяем свойство цвета: @property() color = 'red'; Кроме того, в строке 35 (где мы используем это свойство для определения цвета заливки круга, образующего колпачок кнопки), используя литеральный синтаксис шаблона JavaScript, записанный как ${color} :

 <circle cx="6" cy="6" r="2.9" fill="${color}" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08" />

Делаем это интерактивным

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

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

Инвертирование градиента контура кнопки
Инвертирование градиента контура кнопки (большой предварительный просмотр)

Градиент для контура кнопки определяется следующим кодом SVG, где ${color} заменяется цветом кнопки с помощью lit-element , как описано выше:

 <linearGradient x1="0" x2="1" y1="0" y2="1"> <stop stop-color="#ffffff" offset="0" /> <stop stop-color="${color}" offset="0.3" /> <stop stop-color="${color}" offset="0.5" /> <stop offset="1" /> </linearGradient>

Одним из подходов к нажатой кнопке может быть определение второго градиента, инвертирование порядка цветов и использование его в качестве заливки круга при каждом нажатии кнопки. Однако есть хороший трюк, который позволяет нам повторно использовать один и тот же градиент: мы можем повернуть элемент svg на 180 градусов, используя преобразование SVG:

 <circle cx="6" cy="6" r="3.822" fill="url(#a)" transform="rotate(180 6 6)" />

Атрибут transform сообщает SVG, что мы хотим повернуть круг на 180 градусов, и что поворот должен происходить вокруг точки (6, 6), которая является центром круга (определяется cx и cy ). Преобразования SVG также влияют на заливку формы, поэтому наш градиент также будет вращаться.

Мы хотим инвертировать градиент только при нажатии кнопки, поэтому вместо того, чтобы добавлять атрибут transform непосредственно к элементу <circle> , как мы сделали выше, мы на самом деле установим класс CSS для этого элемента, а затем воспользуемся преимуществом. того факта, что атрибуты SVG можно задавать через CSS, хотя и с использованием немного другого синтаксиса:

 transform: rotate(180deg); transform-origin: 6px 6px;

Эти два правила CSS делают то же самое, что и transform , которое мы использовали выше — поворачиваем круг на 180 градусов вокруг его центра в точке (6, 6). Мы хотим, чтобы эти правила применялись только при нажатии кнопки, поэтому мы добавим имя класса CSS в наш круг:

 <circle class="button-contour" cx="6" cy="6" r="3.822" fill="url(#a)" />

И теперь мы можем использовать псевдокласс :active CSS, чтобы применить преобразование к button-contour всякий раз, когда нажимается элемент SVG:

 svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; }

lit-element позволяет нам прикрепить таблицу стилей к нашему компоненту, объявив ее в статическом геттере внутри нашего класса компонента, используя помеченный литерал шаблона:

 static get styles() { return css` svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; } `; }

Как и шаблон HTML, этот синтаксис позволяет нам вводить пользовательские значения в наш код CSS, хотя здесь он нам не нужен. lit-element также заботится о создании Shadow DOM для нашего компонента, чтобы CSS воздействовал только на элементы внутри нашего компонента и не распространялся на другие части приложения.

А как насчет программного поведения кнопки при нажатии? Мы хотим запускать событие, чтобы пользователи нашего компонента могли понять, когда изменяется состояние кнопки. Один из способов сделать это — прослушивать события mousedown и mouseup для элемента SVG и запускать события «нажатие кнопки»/«отпускание кнопки» соответственно. Вот как это выглядит с синтаксисом lit-element :

 render() { const { color } = this; return html` <svg @mousedown=${() => this.dispatchEvent(new Event('button-press'))} @mouseup=${() => this.dispatchEvent(new Event('button-release'))} ... </svg> `; }

Однако это не лучшее решение, как мы скоро увидим. Но сначала взглянем на код, который мы получили:

 import { customElement, css, html, LitElement, property } from 'lit-element'; @customElement('wokwi-pushbutton') export class PushbuttonElement extends LitElement { @property() color = 'red'; static get styles() { return css` svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; } `; } render() { const { color } = this; return html` <svg @mousedown=${() => this.dispatchEvent(new Event('button-press'))} @mouseup=${() => this.dispatchEvent(new Event('button-release'))} width="18mm" height="12mm" version="1.1" viewBox="-3 0 18 12" xmlns="https://www.w3.org/2000/svg" > <defs> <linearGradient x1="0" x2="1" y1="0" y2="1"> <stop stop-color="#ffffff" offset="0" /> <stop stop-color="${color}" offset="0.3" /> <stop stop-color="${color}" offset="0.5" /> <stop offset="1" /> </linearGradient> </defs> <rect x="0" y="0" width="12" height="12" rx=".44" ry=".44" fill="#464646" /> <rect x=".75" y=".75" width="10.5" height="10.5" rx=".211" ry=".211" fill="#eaeaea" /> <g fill="#1b1b1"> <circle cx="1.767" cy="1.7916" r=".37" /> <circle cx="10.161" cy="1.7916" r=".37" /> <circle cx="10.161" cy="10.197" r=".37" /> <circle cx="1.767" cy="10.197" r=".37" /> </g> <g fill="#eaeaea"> <path d="m-0.3538 1.4672c-0.058299 0-0.10523 0.0469-0.10523 0.10522v0.38698h-2.1504c-0.1166 0-0.21045 0.0938-0.21045 0.21045v0.50721c0 0.1166 0.093855 0.21045 0.21045 0.21045h2.1504v0.40101c0 0.0583 0.046928 0.10528 0.10523 0.10528h0.35723v-1.9266z" /> <path d="m-0.35376 8.6067c-0.058299 0-0.10523 0.0469-0.10523 0.10523v0.38697h-2.1504c-0.1166 0-0.21045 0.0939-0.21045 0.21045v0.50721c0 0.1166 0.093855 0.21046 0.21045 0.21046h2.1504v0.401c0 0.0583 0.046928 0.10528 0.10523 0.10528h0.35723v-1.9266z" /> <path d="m12.354 1.4672c0.0583 0 0.10522 0.0469 0.10523 0.10522v0.38698h2.1504c0.1166 0 0.21045 0.0938 0.21045 0.21045v0.50721c0 0.1166-0.09385 0.21045-0.21045 0.21045h-2.1504v0.40101c0 0.0583-0.04693 0.10528-0.10523 0.10528h-0.35723v-1.9266z" /> <path d="m12.354 8.6067c0.0583 0 0.10523 0.0469 0.10523 0.10522v0.38698h2.1504c0.1166 0 0.21045 0.0938 0.21045 0.21045v0.50721c0 0.1166-0.09386 0.21045-0.21045 0.21045h-2.1504v0.40101c0 0.0583-0.04693 0.10528-0.10523 0.10528h-0.35723v-1.9266z" /> </g> <g> <circle class="button-contour" cx="6" cy="6" r="3.822" fill="url(#a)" /> <circle cx="6" cy="6" r="2.9" fill="${color}" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08" /> </g> </svg> `; } }
  • Посмотреть демо →

Вы можете нажать на каждую из кнопок и посмотреть, как они реагируют. У красного даже есть прослушиватели событий (определенные в index.html ), поэтому, когда вы нажимаете на него, вы должны увидеть некоторые сообщения, записанные в консоль. Но подождите, а что, если вместо этого вы захотите использовать клавиатуру?

Делаем компонент доступным и удобным для мобильных устройств

Ура! Мы создали повторно используемый компонент кнопки с SVG и lit-element !

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

Начнем с решения проблемы с клавиатурой. Мы могли бы сделать кнопку доступной с клавиатуры, добавив атрибут tabindex к элементу svg, сделав его фокусируемым. На мой взгляд, лучшая альтернатива — просто обернуть кнопку стандартным элементом <button> . Используя стандартный элемент, мы также заставляем его хорошо работать с программами чтения с экрана и другими вспомогательными технологиями.

У этого подхода есть один недостаток, как вы можете видеть ниже:

Наш симпатичный компонент заключен внутри элемента кнопки
Наш симпатичный компонент заключен внутри элемента <button> . (Большой превью)

Элемент <button> поставляется с некоторыми встроенными стилями. Это можно легко исправить, применив CSS для удаления этих стилей:

 button { border: none; background: none; padding: 0; margin: 0; text-decoration: none; -webkit-appearance: none; -moz-appearance: none; } button:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; }

Обратите внимание, что мы также заменили селектор, который инвертирует сетку контура кнопок, используя button:active вместо svg:active . Это гарантирует, что стиль нажатия кнопки применяется всякий раз, когда нажимается фактический элемент <button> , независимо от используемого устройства ввода.

Мы даже можем сделать наш компонент более удобным для чтения с экрана, добавив атрибут aria-label , который включает цвет кнопки:

 <button aria-label="${color} pushbutton">

Осталось решить еще одну проблему: события «нажатие кнопки» и «отпускание кнопки». В идеале мы хотим запускать их на основе псевдокласса CSS :active кнопки, точно так же, как мы делали это в CSS выше. Другими словами, мы хотели бы запускать событие «нажатие кнопки» всякий раз, когда кнопка становится :active , и событие «отпускание кнопки» всякий раз, когда оно становится :not(:active) .

Но как прослушать псевдокласс CSS из Javascript?

Оказывается, это не так просто. Я задал этот вопрос сообществу JavaScript Israel и в конце концов откопал одну идею, которая сработала из бесконечной цепочки: используйте селектор :active для запуска суперкороткой CSS-анимации, а затем я могу прослушать ее из JavaScript с помощью animationstart . мероприятие.

Быстрый эксперимент CodePen доказал, что это действительно работает надежно. Как бы мне ни нравилась сложность этой идеи, я решил использовать другое, более простое решение. Событие animationstart недоступно в Edge и iOS Safari, и запуск CSS-анимации только для обнаружения изменения состояния кнопки не кажется правильным способом.

Вместо этого мы добавим в элемент <button> три пары обработчиков событий: mousedown/mouseup для мыши, touchstart/touchend для мобильных устройств и keyup/keydown для клавиатуры. На мой взгляд, не самое элегантное решение, но оно помогает и работает во всех браузерах.

 <button aria-label="${color} pushbutton" @mousedown=${this.down} @mouseup=${this.up} @touchstart=${this.down} @touchend=${this.up} @keydown=${(e: KeyboardEvent) => e.keyCode === SPACE_KEY && this.down()} @keyup=${(e: KeyboardEvent) => e.keyCode === SPACE_KEY && this.up()} >

Где SPACE_KEY — константа, равная 32, а up / down — два метода класса, которые отправляют события button-press и button-release :

 @property() pressed = false; private down() { if (!this.pressed) { this.pressed = true; this.dispatchEvent(new Event('button-press')); } } private up() { if (this.pressed) { this.pressed = false; this.dispatchEvent(new Event('button-release')); } }
  • Вы можете найти полный исходный код здесь.

Мы сделали это!

Это был довольно долгий путь, который начался с определения требований и рисования иллюстрации для кнопки в Inkscape, прошел через преобразование нашего SVG-файла в многократно используемый веб-компонент с помощью lit-element , и после того, как мы убедились, что он доступен и удобен для мобильных устройств, мы в итоге получилось почти 100 строк кода восхитительного компонента виртуальной кнопки.

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

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

Тогда до следующего раза!