Воссоздание кнопки Arduino с использованием SVG и <lit-element>
Опубликовано: 2022-03-10В этой статье вы узнаете, как создавать собственные 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 мм. Эти кнопки очень часто встречаются в стартовых наборах электроники и поставляются с колпачками разных цветов, как вы можете видеть на фотографии ниже:
С точки зрения поведения кнопка должна иметь два состояния: нажата и отпущена. Они аналогичны HTML-событиям mousedown/mouseup, но мы должны убедиться, что кнопки также могут использоваться с мобильных устройств и доступны для пользователей без мыши.
Поскольку мы будем использовать состояние кнопки в качестве входных данных для Arduino, нет необходимости поддерживать события «щелчок» или «двойной щелчок». Программа Arduino, работающая в симуляции, решает, как действовать в зависимости от состояния кнопки, а физические кнопки не генерируют события щелчка.
Если вы хотите узнать больше, ознакомьтесь с докладом, который я провел с Бенджамином Грюнбаумом на SmashingConf Freiburg в 2019 году: «Анатомия клика».
Подводя итог нашим требованиям, наша кнопка должна:
- похожи на физическую кнопку 12 мм;
- иметь два различных состояния: нажатое и отпущенное, и они должны быть визуально различимы;
- поддерживать взаимодействие с мышью, мобильными устройствами и быть доступным для пользователей клавиатуры;
- поддерживать разные цвета крышек (по крайней мере, красный, зеленый, синий, желтый, белый и черный).
Теперь, когда мы определили требования, мы можем начать работу над реализацией.
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 довольно просто. Мы собираемся нарисовать вид сверху кнопки и ее четырех металлических выводов, которые соединяют ее с другими частями, следующим образом:
- Темно-серый прямоугольник размером 12×12 мм для пластикового корпуса со слегка закругленными углами для большей мягкости.
- Меньший светло-серый прямоугольник размером 10,5 × 10,5 для металлической крышки.
- Четыре более темных круга, по одному в каждом углу для штифтов, удерживающих кнопку вместе.
- Большой круг посередине, это контур пуговицы.
- Меньший круг посередине для верхней части пуговицы.
- Четыре светло-серых прямоугольника в форме буквы «Т» для металлических выводов кнопки.
И результат, немного увеличенный:
В качестве последнего штриха мы добавим магический градиент SVG к контуру кнопки, чтобы придать ей ощущение трехмерности:
Ну вот! У нас есть визуальные эффекты, теперь нам нужно разместить их в Интернете.
От Inkscape к веб-SVG
Как я упоминал выше, SVG довольно просто встроить в HTML — вы можете просто вставить содержимое файла SVG в свой HTML-документ, открыть его в браузере, и он будет отображаться на вашем экране. Вы можете увидеть это в действии в следующем примере CodePen:
Однако файлы SVG, сохраненные из Inkscape, содержат много ненужного багажа, такого как версия Inkscape и положение окна при последнем сохранении файла. Во многих случаях есть также пустые элементы, неиспользуемые градиенты и фильтры, и все они увеличивают размер файла и затрудняют работу с ним внутри HTML.
К счастью, Inkscape может убрать за нас большую часть беспорядка. Вот как это сделать:
- Перейдите в меню « Файл» и нажмите « Очистить документ» . (Это удалит неиспользуемые определения из вашего документа.)
- Снова перейдите в « Файл» и нажмите « Сохранить как… ». При сохранении выберите Оптимизированный SVG ( *.svg ) в раскрывающемся списке Тип файла.
- Вы увидите диалоговое окно «Оптимизированный вывод 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>
поставляется с некоторыми встроенными стилями. Это можно легко исправить, применив 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, который я сейчас разрабатываю, где вы также можете увидеть кнопку в действии.
Тогда до следующего раза!