Recreando el pulsador de Arduino usando SVG y <lit-element>
Publicado: 2022-03-10En este artículo, aprenderá a crear componentes HTML personalizados que imitan objetos físicos, como el botón Arduino. Dibujaremos el componente en Inkscape desde cero, optimizaremos el código SVG generado para la Web y lo empaquetaremos como un componente web independiente utilizando la biblioteca ligera
lit-element
, prestando especial atención a las consideraciones de accesibilidad y usabilidad móvil. Hoy, lo llevaré a través del viaje de creación de un componente HTML que imita un componente de botón pulsador momentáneo que se usa comúnmente con Arduino y en proyectos electrónicos. Usaremos tecnologías como SVG, Web Components y lit-element
, y aprenderemos cómo hacer que el botón sea accesible a través de algunos trucos de JavaScript-CSS.
¡Empecemos!
De Arduino a HTML: la necesidad de un componente de botón pulsador
Antes de embarcarnos en el viaje, exploremos qué vamos a crear y, lo que es más importante, por qué. Estoy creando un simulador Arduino de código abierto en JavaScript llamado avr8js. Este simulador es capaz de ejecutar código Arduino y lo usaré en una serie de tutoriales y cursos que enseñan a los creadores cómo programar para Arduino.
El simulador en sí solo se ocupa de la ejecución del programa: ejecuta el código instrucción por instrucción y actualiza su estado interno y un búfer de memoria de acuerdo con la lógica del programa. Para interactuar con el programa Arduino, debe crear algunos componentes electrónicos virtuales que puedan enviar entradas al simulador o reaccionar a sus salidas.
Ejecutar el simulador solo es muy parecido a ejecutar JavaScript de forma aislada. Realmente no puede interactuar con el usuario a menos que también cree algunos elementos HTML y los conecte al código JavaScript a través del DOM.
Así, además del simulador del procesador, también estoy trabajando en una librería de componentes HTML que imitan el hardware físico, empezando por los dos primeros componentes que encontrarás en casi cualquier proyecto de electrónica: un LED y un pulsador.
El LED es relativamente simple, ya que solo tiene dos estados de salida: encendido y apagado. Detrás de escena, utiliza un filtro SVG para crear el efecto de iluminación.
El pulsador es más interesante. También tiene dos estados, pero tiene que reaccionar a la entrada del usuario y actualizar su estado en consecuencia, y aquí es donde surge el desafío, como veremos en breve. Pero primero, determinemos los requisitos de nuestro componente que vamos a crear.
Definición de los requisitos para el pulsador
Nuestro componente se parecerá a un botón pulsador de 12 mm. Estos botones son muy comunes en los kits de inicio de electrónica y vienen con tapas en varios colores, como puede ver en la foto a continuación:
En términos de comportamiento, el botón pulsador debe tener dos estados: presionado y liberado. Estos son similares a los eventos HTML mousedown/mouseup, pero debemos asegurarnos de que los botones también se puedan usar desde dispositivos móviles y que sean accesibles para usuarios sin mouse.
Como usaremos el estado del botón pulsador como entrada para Arduino, no es necesario admitir eventos de "clic" o "doble clic". Depende del programa Arduino que se ejecuta en la simulación decidir cómo actuar sobre el estado del botón, y los botones físicos no generan eventos de clic.
Si desea obtener más información, consulte una charla que sostuve con Benjamin Gruenbaum en SmashingConf Freiburg en 2019: "Anatomía de un clic".
Para resumir nuestros requisitos, nuestro pulsador debe:
- parecerse al botón pulsador físico de 12 mm;
- tener dos estados distintos: presionado y liberado, y deben ser perceptibles visualmente;
- admitir la interacción del mouse, los dispositivos móviles y ser accesible para los usuarios del teclado;
- admitir diferentes colores de tapa (al menos rojo, verde, azul, amarillo, blanco y negro).
Ahora que hemos definido los requisitos, podemos comenzar a trabajar en la implementación.
SVG para la victoria
La mayoría de los componentes web se implementan mediante una combinación de CSS y HTML. Cuando necesitamos gráficos más complejos, generalmente usamos imágenes rasterizadas, ya sea en formato JPG o PNG (o GIF si siente nostalgia).
En nuestro caso, sin embargo, utilizaremos otro enfoque: gráficos SVG. SVG se presta a gráficos complejos mucho más fácilmente que CSS (sí, lo sé, puedes crear cosas fascinantes con CSS, pero eso no significa que debas hacerlo). Pero no te preocupes, no renunciaremos por completo a CSS. Nos ayudará a diseñar los botones y, finalmente, incluso a hacerlos accesibles.
SVG tiene otra gran ventaja, en comparación con las imágenes de gráficos de trama: es muy fácil de manipular desde JavaScript y se puede diseñar a través de CSS. Esto significa que podemos proporcionar una sola imagen para el botón y usar JavaScript para personalizar la tapa de color y los estilos CSS para indicar el estado del botón. Genial, ¿no?
Finalmente, SVG es solo un documento XML, que se puede editar con editores de texto e incrustar directamente en HTML, lo que lo convierte en una solución perfecta para crear componentes HTML reutilizables. ¿Estás listo para dibujar nuestro pulsador?
Dibujar el pulsador con Inkscape
Inkscape es mi herramienta favorita para crear gráficos vectoriales SVG. Es gratis y está repleto de potentes funciones, como una gran colección de ajustes preestablecidos de filtro incorporados, rastreo de mapas de bits y operaciones binarias de ruta. Empecé a usar Inkscape para crear arte de PCB, pero en los últimos dos años, comencé a usarlo para la mayoría de mis tareas de edición de gráficos.
Dibujar el botón en Inkscape es bastante sencillo. Vamos a dibujar una ilustración de vista superior del botón y sus cuatro cables metálicos que lo conectan a otras partes, de la siguiente manera:
- Rectángulo gris oscuro de 12×12 mm para la caja de plástico, con esquinas ligeramente redondeadas para hacerlo más suave.
- Rectángulo gris claro más pequeño de 10,5 × 10,5 para la cubierta de metal.
- Cuatro círculos más oscuros, uno en cada esquina para los pines que mantienen unido el botón.
- Un círculo grande en el medio, ese es el contorno de la tapa del botón.
- Un círculo más pequeño en el medio para la parte superior de la tapa del botón.
- Cuatro rectángulos de color gris claro en forma de “T” para los cables metálicos del botón.
Y el resultado, ligeramente ampliado:
Como toque final, agregaremos un poco de magia de degradado SVG al contorno del botón, para darle una sensación 3D:
¡Aquí vamos! Tenemos las imágenes, ahora tenemos que llevarlas a la web.
De Inkscape a Web SVG
Como mencioné anteriormente, los SVG son bastante sencillos de incrustar en HTML: simplemente puede pegar el contenido del archivo SVG en su documento HTML, abrirlo en un navegador y se mostrará en su pantalla. Puede verlo en acción en el siguiente ejemplo de CodePen:
Sin embargo, los archivos SVG guardados desde Inkscape contienen mucho equipaje innecesario, como la versión de Inkscape y la posición de la ventana cuando guardó el archivo por última vez. En muchos casos, también hay elementos vacíos, degradados y filtros sin usar, y todos aumentan el tamaño del archivo y dificultan el trabajo con él dentro de HTML.
Afortunadamente, Inkscape puede limpiar la mayor parte del desorden por nosotros. Así es como lo haces:
- Vaya al menú Archivo y haga clic en Limpiar documento . (Esto eliminará las definiciones no utilizadas de su documento).
- Vaya de nuevo a Archivo y haga clic en Guardar como… . Al guardar, seleccione SVG optimizado ( *.svg ) en el menú desplegable Guardar como tipo .
- Verá un cuadro de diálogo "Salida SVG optimizada" con tres pestañas. Marque todas las opciones, excepto "Conservar datos del editor", "Conservar definiciones no referenciadas" y "Conservar ID creados manualmente...".
Eliminar todas estas cosas creará un archivo SVG más pequeño con el que será más fácil trabajar. En mi caso, el archivo pasó de 4593 bytes a solo 2080 bytes, menos de la mitad del tamaño. Para archivos SVG más complejos, esto puede significar un gran ahorro de ancho de banda y puede marcar una diferencia notable en el tiempo de carga de su página web.
El SVG optimizado también es mucho más fácil de leer y comprender. En el siguiente extracto, debería poder ver fácilmente los dos rectángulos que forman el cuerpo del botón pulsador:
<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"/>
Incluso puede acortar aún más el código, por ejemplo, cambiando el ancho del trazo del primer rectángulo de 1.0003
a solo 1
. No hace una diferencia significativa en el tamaño del archivo, pero hace que el código sea más fácil de leer.
En general, siempre es útil pasar manualmente el archivo SVG generado. En muchos casos, puede eliminar grupos vacíos o aplicar transformaciones de matriz, así como simplificar las coordenadas de degradado asignándolas desde "espacio de usuario en uso" (coordenadas globales) hasta "cuadro delimitador de objeto" (en relación con el objeto). Estas optimizaciones son opcionales, pero obtiene un código que es más fácil de entender y mantener.
A partir de este momento, guardaremos Inkscape y trabajaremos con la representación de texto de la imagen SVG.
Creación de un componente web reutilizable
Hasta ahora tenemos los gráficos de nuestro pulsador, listos para ser insertados en nuestro simulador. Podemos personalizar fácilmente el color del botón cambiando el atributo de fill
del círculo más pequeño y el color de inicio del degradado del círculo más grande.
Nuestro siguiente objetivo es convertir nuestro pulsador en un componente web reutilizable que se puede personalizar pasando un atributo de color
y reacciona a la interacción del usuario (eventos de pulsación/liberación). Usaremos lit-element
, una pequeña librería que simplifica la creación de Web Components.
lit-element
sobresale en la creación de pequeñas bibliotecas de componentes independientes. Está construido sobre el estándar de componentes web, que permite que cualquier aplicación web consuma estos componentes, independientemente del marco utilizado: Angular, React, Vue o Vanilla JS podrían usar nuestro componente.
La creación de componentes en lit-element
se realiza mediante una sintaxis basada en clases, con un método render()
que devuelve el código HTML del elemento. Un poco similar a React, si está familiarizado con él. Sin embargo, a diferencia de react, lit-element
utiliza literales de plantilla etiquetados de Javascript estándar para definir el contenido del componente.
Así es como crearía un componente simple hello-world
:
import { customElement, html, LitElement } from 'lit-element'; @customElement('hello-world') export class HelloWorldElement extends LitElement { render() { return html` <h1> Hello, World! </h1> `; } }
Este componente se puede usar en cualquier parte de su código HTML simplemente escribiendo <hello-world></hello-world>
.
Nota : En realidad, nuestro botón requiere solo un poco más de código: necesitamos declarar una propiedad de entrada para el color, usando el @property()
(y con un valor predeterminado de rojo), y pegar el código SVG en nuestro render()
, reemplazando el color de la tapa del botón con el valor de la propiedad color (ver ejemplo). Los bits importantes están en la línea 5, donde definimos la propiedad de color: @property() color = 'red';
Además, en la línea 35 (donde usamos esta propiedad para definir el color de relleno del círculo que forma la tapa del botón), usando la sintaxis literal de la plantilla de JavaScript, escrita como ${color}
:
<circle cx="6" cy="6" r="2.9" fill="${color}" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08" />
Haciéndolo interactivo
La última pieza del rompecabezas sería hacer que el botón fuera interactivo. Hay dos aspectos que debemos considerar: la respuesta visual a la interacción, así como la respuesta programática a la interacción.
Para la parte visual, simplemente podemos invertir el relleno degradado del contorno del botón, lo que creará la ilusión de que se ha presionado el botón:
El degradado del contorno del botón se define mediante el siguiente código SVG, donde ${color}
se reemplaza con el color del botón mediante lit-element
, como se explicó anteriormente:
<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>
Un enfoque para la apariencia del botón presionado sería definir un segundo degradado, invertir el orden de los colores y usarlo como relleno del círculo cada vez que se presiona el botón. Sin embargo, hay un buen truco que nos permite reutilizar el mismo degradado: podemos rotar el elemento svg 180 grados usando una transformación SVG:
<circle cx="6" cy="6" r="3.822" fill="url(#a)" transform="rotate(180 6 6)" />
El atributo de transform
le dice a SVG que queremos rotar el círculo 180 grados y que la rotación debe ocurrir alrededor del punto (6, 6) que es el centro del círculo (definido por cx
y cy
). Las transformaciones de SVG también afectan el relleno de la forma, por lo que nuestro degradado también se rotará.
Solo queremos invertir el degradado cuando se presiona el botón, por lo que en lugar de agregar el atributo de transform
directamente en el elemento <circle>
, como hicimos anteriormente, vamos a establecer una clase CSS para este elemento y luego aprovechar del hecho de que los atributos SVG se pueden configurar a través de CSS, aunque usando una sintaxis ligeramente diferente:
transform: rotate(180deg); transform-origin: 6px 6px;
Estas dos reglas CSS hacen exactamente lo mismo que la transform
que teníamos arriba: girar el círculo 180 grados alrededor de su centro en (6, 6). Queremos que estas reglas se apliquen solo cuando se presione el botón, por lo que agregaremos un nombre de clase CSS a nuestro círculo:
<circle class="button-contour" cx="6" cy="6" r="3.822" fill="url(#a)" />
Y ahora podemos usar la pseudoclase :active CSS para aplicar una transformación al button-contour
cada vez que se hace clic en el elemento SVG:
svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; }
lit-element
nos permite adjuntar una hoja de estilo a nuestro componente declarándola en un getter estático dentro de nuestra clase de componente, usando un literal de plantilla etiquetado:
static get styles() { return css` svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; } `; }
Al igual que la plantilla HTML, esta sintaxis nos permite inyectar valores personalizados a nuestro código CSS, aunque no lo necesitemos aquí. lit-element
también se encarga de crear Shadow DOM para nuestro componente, de modo que el CSS solo afecte a los elementos dentro de nuestro componente y no se filtre a otras partes de la aplicación.
Ahora, ¿qué pasa con el comportamiento programático del botón cuando se presiona? Queremos activar un evento para que los usuarios de nuestro componente puedan saber cuándo cambia el estado del botón. Una forma de hacer esto es escuchar los eventos mousedown y mouseup en el elemento SVG, y activar los eventos "pulsar botón"/"soltar botón" correspondientemente. Así es como se ve con la sintaxis lit-element
:
render() { const { color } = this; return html` <svg @mousedown=${() => this.dispatchEvent(new Event('button-press'))} @mouseup=${() => this.dispatchEvent(new Event('button-release'))} ... </svg> `; }
Sin embargo, esta no es la mejor solución, como veremos en breve. Pero primero, eche un vistazo rápido al código que tenemos hasta ahora:
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> `; } }
- Ver demostración →
Puede hacer clic en cada uno de los botones y ver cómo reaccionan. El rojo incluso tiene algunos detectores de eventos (definidos en index.html ), por lo que cuando haga clic en él debería ver algunos mensajes escritos en la consola. Pero espera, ¿qué pasa si quieres usar el teclado en su lugar?
Hacer que el componente sea accesible y apto para dispositivos móviles
¡Hurra! ¡Creamos un componente de botón pulsador reutilizable con SVG y lit-element
!
Antes de aprobar nuestro trabajo, hay algunos problemas que debemos analizar. Primero, el botón no es accesible para las personas que usan el teclado. Además, el comportamiento en dispositivos móviles es inconsistente: los botones aparecen presionados cuando mantienes el dedo sobre ellos, pero los eventos de JavaScript no se activan si mantienes el dedo durante más de un segundo.
Empecemos por abordar el tema del teclado. Podríamos hacer que el botón sea accesible desde el teclado agregando un atributo tabindex al elemento svg, haciéndolo enfocable. Una mejor alternativa, en mi opinión, es envolver el botón con un elemento <button>
estándar. Al usar el elemento estándar, también hacemos que funcione bien con lectores de pantalla y otras tecnologías de asistencia.
Este enfoque tiene un inconveniente, como puede ver a continuación:
El elemento <button>
viene con un estilo incorporado. Esto podría solucionarse fácilmente aplicando algo de CSS para eliminar estos estilos:
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; }
Tenga en cuenta que también reemplazamos el selector que invierte la cuadrícula del contorno de los botones, usando button:active
en lugar de svg:active
. Esto garantiza que el estilo de botón presionado se aplique siempre que se presione el elemento <button>
real, independientemente del dispositivo de entrada utilizado.
Incluso podemos hacer que nuestro componente sea más fácil de leer en pantalla agregando un atributo aria-label
que incluya el color del botón:
<button aria-label="${color} pushbutton">
Todavía hay una cosa más que abordar: los eventos de "pulsar botón" y "soltar botón". Idealmente, queremos activarlos en función del CSS: pseudoclase activa del botón, tal como lo hicimos en el CSS anterior. En otras palabras, nos gustaría activar el evento "pulsar botón" cada vez que el botón se vuelve :active
, y el evento "liberar botón" se activa cada vez que :not(:active)
.
Pero, ¿cómo escuchas una pseudoclase CSS de Javascript?
Resulta que no es tan simple. Hice esta pregunta a la comunidad de JavaScript Israel y, finalmente, desenterré una idea que funcionó a partir del hilo sin fin: use el selector :active
para activar una animación CSS súper corta, y luego puedo escucharla desde JavaScript usando el inicio de animationstart
. evento.
Un experimento rápido de CodePen demostró que esto realmente funciona de manera confiable. Por mucho que me gustara la sofisticación de esta idea, decidí ir con una solución diferente y más simple. El evento animationstart
no está disponible en Edge y iOS Safari, y activar una animación CSS solo para detectar el cambio de estado del botón no parece la forma correcta de hacer las cosas.
En su lugar, agregaremos tres pares de detectores de eventos al elemento <button>
: mousedown/mouseup para el mouse, touchstart/touchend para dispositivos móviles y keyup/keydown para el teclado. No es la solución más elegante, en mi opinión, pero funciona y funciona en todos los navegadores.
<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()} >
Donde SPACE_KEY
es una constante que equivale a 32, y up
/ down
son dos métodos de clase que distribuyen los eventos button-press
y 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')); } }
- Puede encontrar el código fuente completo aquí.
¡Lo hicimos!
Fue un viaje bastante largo que comenzó con la descripción de los requisitos y el dibujo de la ilustración para el botón en Inkscape, pasó por convertir nuestro archivo SVG en un componente web reutilizable usando lit-element
y, después de asegurarnos de que sea accesible y compatible con dispositivos móviles, terminó con casi 100 líneas de código de un delicioso componente de botón virtual.
Este botón es solo un componente en una biblioteca de código abierto de componentes electrónicos virtuales que estoy construyendo. Lo invitamos a echar un vistazo al código fuente o consultar el Storybook en línea donde puede ver e interactuar con todos los componentes disponibles.
Y por último, si estás interesado en Arduino, echa un vistazo al curso de programación de Simon para Arduino que estoy construyendo actualmente, donde también puedes ver el pulsador en acción.
¡Hasta la próxima, entonces!