使用 SVG 和 <lit-element> 重新创建 Arduino 按钮

已发表: 2022-03-10
快速总结 ↬ HTML 带有一堆输入控件,并且有大量的组件库,其中包含许多标准控件,例如复选框和单选按钮。 但是当你需要一些不寻常的东西时会发生什么?

在本文中,您将学习如何构建模仿物理对象的自定义 HTML 组件,例如 Arduino 按钮。 我们将在 Inkscape 中从头开始绘制组件,优化 Web 生成的 SVG 代码,并使用轻量级lit-element库将其包装为独立的 Web 组件,特别注意可访问性和移动可用性方面的考虑。

今天,我将带您完成创建一个 HTML 组件的旅程,该组件模仿 Arduino 和电子项目中常用的瞬时按钮组件。 我们将使用 SVG、Web 组件和lit-element等技术,并学习如何通过一些 JavaScript-CSS 技巧使按钮可访问。

开始吧!

从 Arduino 到 HTML:需要一个按钮组件

在我们踏上旅程之前,让我们探索一下我们将要创造什么,更重要的是,为什么。 我正在用 JavaScript 创建一个名为 avr8js 的开源 Arduino 模拟器。 这个模拟器能够执行 Arduino 代码,我将在一系列教程和课程中使用它,教创客如何为 Arduino 编程。

模拟器本身只负责程序的执行——它按指令运行代码,并根据程序逻辑更新其内部状态和内存缓冲区。 为了与 Arduino 程序进行交互,您需要创建一些虚拟电子组件,这些组件可以将输入发送到模拟器或对其输出做出反应。

单独运行模拟器与单独运行 JavaScript 非常相似。 除非您还创建一些 HTML 元素,并通过 DOM 将它们与 JavaScript 代码挂钩,否则您无法真正与用户交互。

因此,除了处理器的模拟器之外,我还在研究模拟物理硬件的 HTML 组件库,从几乎在任何电子项目中都能找到的前两个组件开始:LED 和按钮。

LED 和按钮元件在起作用
LED 和按钮元件在起作用

LED 相对简单,因为它只有两种输出状态:开和关。 在幕后,它使用 SVG 过滤器来创建照明效果。

按钮更有趣。 它也有两种状态,但它必须对用户输入做出反应并相应地更新其状态,这就是挑战的来源,我们很快就会看到。 但首先,让我们确定我们将要创建的组件的需求。

跳跃后更多! 继续往下看↓

定义按钮的要求

我们的组件将类似于一个 12 毫米的按钮。 这些按钮在电子入门套件中非常常见,并带有多种颜色的盖子,如下图所示:

带有黄色、红色、蓝色和绿色按钮的西蒙游戏
带有黄色、红色、蓝色和绿色按钮的西蒙游戏(大预览)

在行为方面,按钮应该有两种状态:按下和释放。 这些类似于 mousedown/mouseup HTML 事件,但我们必须确保按钮也可以在移动设备上使用,并且可供没有鼠标的用户访问。

由于我们将使用按钮的状态作为 Arduino 的输入,因此无需支持“单击”或“双击”事件。 由模拟中运行的 Arduino 程序决定如何对按钮的状态进行操作,物理按钮不会产生点击事件。

如果您想了解更多信息,请查看我与 Benjamin Gruenbaum 于 2019 年在 SmashingConf Freiburg 举行的演讲:“点击剖析”。

总结一下我们的要求,我们的按钮需要:

  1. 看起来类似于物理 12 毫米按钮;
  2. 有两种不同的状态:按下和释放,它们应该是视觉可辨的;
  3. 支持鼠标交互、移动设备和键盘用户可以访问;
  4. 支持不同的帽子颜色(至少红色、绿色、蓝色、黄色、白色和黑色)。

现在我们已经定义了需求,我们可以开始着手实现了。

SVG 为胜利

大多数 Web 组件都是使用 CSS 和 HTML 的组合来实现的。 当我们需要更复杂的图形时,我们通常使用 JPG 或 PNG 格式的光栅图像(如果您觉得怀旧,也可以使用 GIF)。

然而,在我们的例子中,我们将使用另一种方法:SVG 图形。 SVG 比 CSS 更容易适用于复杂的图形(是的,我知道,您可以使用 CSS 创建迷人的东西,但这并不意味着它应该这样做)。 但别担心,我们并没有完全放弃 CSS。 它将帮助我们设计按钮的样式,甚至最终使它们易于访问。

与光栅图形图像相比,SVG 有另一个很大的优势:它很容易通过 JavaScript 进行操作,并且可以通过 CSS 设置样式。 这意味着我们可以为按钮提供单个图像,并使用 JavaScript 自定义颜色上限,并使用 CSS 样式来指示按钮的状态。 整洁,不是吗?

最后,SVG 只是一个 XML 文档,可以使用文本编辑器进行编辑,并直接嵌入到 HTML 中,使其成为创建可重用 HTML 组件的完美解决方案。 你准备好画我们的按钮了吗?

使用 Inkscape 绘制按钮

Inkscape 是我最喜欢的用于创建 SVG 矢量图形的工具。 它是免费的,并且具有强大的功能,例如大量的内置过滤器预设、位图跟踪和路径二进制操作。 我开始使用 Inkscape 来创建 PCB 艺术,但在过去的两年里,我开始将它用于我的大部分图形编辑任务。

在 Inkscape 中绘制按钮非常简单。 我们将绘制按钮及其连接到其他部分的四根金属引线的俯视图,如下所示:

  1. 塑料外壳为 12×12mm 深灰色矩形,边角略圆,使其更柔软。
  2. 较小的 10.5×10.5 浅灰色矩形金属盖。
  3. 四个较暗的圆圈,每个角落一个用于将按钮固定在一起的引脚。
  4. 中间一个大圆圈,就是扣帽的轮廓。
  5. 按钮帽顶部中间的一个较小的圆圈。
  6. 四个“T”形的浅灰色矩形用于按钮的金属引线。

结果,稍微放大:

我们的手绘按钮草图
我们的手绘按钮草图(大预览)

最后,我们将在按钮的轮廓上添加一些 SVG 渐变魔法,使其具有 3D 感觉:

添加渐变填充以创建 3D 感觉
添加渐变填充以创建 3D 感觉(大预览)

我们去吧! 我们有了视觉效果,现在我们需要把它放到网络上。

从 Inkscape 到 Web SVG

正如我上面提到的,SVG 嵌入 HTML 非常简单——您只需将 SVG 文件的内容粘贴到 HTML 文档中,在浏览器中打开它,它就会呈现在您的屏幕上。 您可以在以下 CodePen 示例中看到它的实际效果:

请参阅@Uri Shaked 的 HTML 中的 Pen SVG 按钮

请参阅@Uri Shaked 的 HTML 中的 Pen SVG 按钮

但是,从 Inkscape 保存的 SVG 文件包含许多不必要的包袱,例如 Inkscape 版本和上次保存文件时的窗口位置。 在许多情况下,还有空元素、未使用的渐变和过滤器,它们都会增大文件大小,并使其在 HTML 中更难使用。

幸运的是,Inkscape 可以为我们清理大部分的烂摊子。 这是您的操作方法:

  1. 转到“文件”菜单,然后单击“清理文档” 。 (这将从您的文档中删除未使用的定义。)
  2. 再次转到文件并单击另存为...。 保存时,在Save as type下拉菜单中选择Optimized SVG ( *.svg )。
  3. 您将看到一个带有三个选项卡的“优化的 SVG 输出”对话框。 检查所有选项,除了“保留编辑器数据”、“保留未引用的定义”和“保留手动创建的 ID...”。
(大预览)

删除所有这些内容将创建一个更小且更易于使用的 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 图像的文本表示。

创建可重用的 Web 组件

到目前为止,我们得到了按钮的图形,准备好插入到我们的模拟器中。 我们可以通过改变小圆圈的fill属性和大圆圈渐变的起始颜色来轻松自定义按钮的颜色。

我们的下一个目标是将我们的按钮变成一个可重用的 Web 组件,它可以通过传递color属性来定制,并对用户交互(按下/释放事件)做出反应。 我们将使用lit-element ,这是一个简化 Web 组件创建的小型库。

lit-element擅长创建小型独立组件库。 它建立在 Web Components 标准之上,允许任何 Web 应用程序使用这些组件,无论使用什么框架:Angular、React、Vue 或 Vanilla JS 都可以使用我们的组件。

lit-element中创建组件是使用基于类的语法完成的,使用render()方法返回元素的 HTML 代码。 有点类似于 React,如果你熟悉的话。 然而,与 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> `; } }

然后,只需编写<hello-world></hello-world>就可以在 HTML 代码中的任何地方使用该组件。

注意实际上,我们的按钮只需要更多代码:我们需要声明一个颜色的输入属性,使用@property @property()装饰器(并且默认值为 red),并将 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 变换将 svg 元素旋转 180 度:

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

transform属性告诉 SVG 我们要将圆旋转 180 度,并且旋转应该围绕圆心(由cxcy定义)的点 (6, 6) 进行。 SVG 变换也会影响形状的填充,因此我们的渐变也会被旋转。

我们只想在按下按钮时反转渐变,所以不是像上面那样直接在<circle>元素上添加transform属性,我们实际上是要为这个元素设置一个 CSS 类,然后利用SVG 属性可以通过 CSS 设置,尽管使用的语法略有不同:

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

这两个 CSS 规则的作用与我们上面的transform完全相同——在 (6, 6) 处将圆围绕其中心旋转 180 度。 我们希望这些规则仅在按下按钮时应用,因此我们将在我们的圈子中添加一个 CSS 类名称:

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

现在我们可以使用 :active CSS 伪类在单击 SVG 元素时对button-contour应用变换:

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

lit-element允许我们通过在组件类中的静态 getter 中声明样式表来将样式表附加到我们的组件,使用标记的模板字面量:

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

就像 HTML 模板一样,这种语法允许我们将自定义值注入到我们的 CSS 代码中,即使我们在这里不需要它。 lit-element还负责为我们的组件创建 Shadow DOM,这样 CSS 只会影响我们组件内的元素,而不会影响到应用程序的其他部分。

现在,按下按钮时的编程行为呢? 我们想触发一个事件,以便我们组件的用户可以在按钮状态发生变化时弄清楚。 一种方法是监听 SVG 元素上的 mousedown 和 mouseup 事件,并相应地触发“button-press”/“button-release”事件。 这是使用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 事件。

让我们从解决键盘问题开始。 我们可以通过向 svg 元素添加 tabindex 属性来使按钮可通过键盘访问,使其可聚焦。 在我看来,更好的选择就是用标准的<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时触发“button-press”事件,并在变为:not(:active)时触发“button-release”事件。

但是你如何从 Javascript 中收听 CSS 伪类呢?

事实证明,事情并非如此简单。 我向 JavaScript Israel 社区提出了这个问题,最终从无休止的线程中挖掘出一个想法:使用:active选择器触发超短 CSS 动画,然后我可以使用animationstart从 JavaScript 中收听它事件。

一个快速的 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-pressbutton-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 中概述需求和绘制按钮插图开始,使用lit-element将我们的 SVG 文件转换为可重用的 Web 组件,在确保它可访问且对移动设备友好之后,我们最终得到了一个令人愉快的虚拟按钮组件的近 100 行代码。

这个按钮只是我正在构建的虚拟电子组件开源库中的一个组件。 邀请您查看源代码,或查看在线故事书,您可以在其中查看所有可用组件并与之交互。

最后,如果您对 Arduino 感兴趣,请查看我目前正在为 Arduino 编程的 Simon 课程,您还可以在其中看到按钮的运行情况。

那就等下次吧!