Recriando o botão Arduino usando SVG e <lit-element>
Publicados: 2022-03-10Neste artigo, você aprenderá a construir componentes HTML personalizados que imitam objetos físicos, como o botão Arduino. Vamos desenhar o componente no Inkscape do zero, otimizar o código SVG gerado para a Web e envolvê-lo como um componente da Web autônomo usando a leve biblioteca
lit-element
, prestando atenção extra às considerações de acessibilidade e usabilidade móvel. Hoje, vou guiá-lo pela jornada de criação de um componente HTML que imita um componente de botão momentâneo que é comumente usado com Arduino e em projetos eletrônicos. Usaremos tecnologias como SVG, Web Components e lit-element
e aprenderemos como tornar o botão acessível através de alguns truques JavaScript-CSS.
Vamos começar!
Do Arduino ao HTML: a necessidade de um componente de botão
Antes de embarcarmos na jornada, vamos explorar o que vamos criar e, mais importante, por quê. Estou criando um simulador Arduino de código aberto em JavaScript chamado avr8js. Este simulador é capaz de executar código Arduino e eu o usarei em uma série de tutoriais e cursos que ensinam os fabricantes a programar para Arduino.
O próprio simulador cuida apenas da execução do programa — ele executa o código instrução por instrução e atualiza seu estado interno e um buffer de memória de acordo com a lógica do programa. Para interagir com o programa Arduino, você precisa criar alguns componentes eletrônicos virtuais que podem enviar entradas para o simulador ou reagir às suas saídas.
Executar o simulador sozinho é muito parecido com executar o JavaScript isoladamente. Você não pode realmente interagir com o usuário a menos que você também crie alguns elementos HTML e os conecte ao código JavaScript por meio do DOM.
Assim, além do simulador do processador, também estou trabalhando em uma biblioteca de componentes HTML que imitam o hardware físico, começando pelos dois primeiros componentes que você encontra em quase qualquer projeto de eletrônica: um LED e um botão.
O LED é relativamente simples, pois possui apenas dois estados de saída: ligado e desligado. Nos bastidores, ele usa um filtro SVG para criar o efeito de iluminação.
O botão é mais interessante. Ele também tem dois estados, mas precisa reagir à entrada do usuário e atualizar seu estado de acordo, e é daí que vem o desafio, como veremos em breve. Mas primeiro, vamos definir os requisitos do nosso componente que vamos criar.
Definindo os requisitos para o botão
Nosso componente será semelhante a um botão de 12 mm. Esses botões são muito comuns em kits para iniciantes de eletrônicos, e vêm com tampas em várias cores, como você pode ver na foto abaixo:
Em termos de comportamento, o botão deve ter dois estados: pressionado e liberado. Eles são semelhantes aos eventos HTML mousedown/mouseup, mas devemos ter certeza de que os botões também podem ser usados a partir de dispositivos móveis e são acessíveis para usuários sem mouse.
Como usaremos o estado do botão como entrada para o Arduino, não há necessidade de suporte a eventos de "clique" ou "clique duplo". Cabe ao programa Arduino em execução na simulação decidir como agir sobre o estado do botão, e os botões físicos não geram eventos de clique.
Se você quiser saber mais, confira uma palestra que tive com Benjamin Gruenbaum na SmashingConf Freiburg em 2019: “Anatomia de um clique”.
Para resumir nossos requisitos, nosso botão precisa:
- semelhante ao botão físico de 12 mm;
- têm dois estados distintos: pressionado e liberado, e devem ser visualmente discerníveis;
- suportar interação com mouse, dispositivos móveis e ser acessível a usuários de teclado;
- suportam diferentes cores de tampa (pelo menos vermelho, verde, azul, amarelo, branco e preto).
Agora que definimos os requisitos, podemos começar a trabalhar na implementação.
SVG para a vitória
A maioria dos componentes da web são implementados usando uma combinação de CSS e HTML. Quando precisamos de gráficos mais complexos, geralmente usamos imagens raster, no formato JPG ou PNG (ou GIF se você se sentir nostálgico).
No nosso caso, porém, usaremos outra abordagem: gráficos SVG. SVG se presta a gráficos complexos muito mais facilmente do que CSS (sim, eu sei, você pode criar coisas fascinantes com CSS, mas isso não significa que deveria). Mas não se preocupe, não estamos desistindo totalmente do CSS. Isso nos ajudará a estilizar os botões e, eventualmente, até torná-los acessíveis.
SVG tem outra grande vantagem, em comparação com imagens gráficas raster: é muito fácil de manipular a partir de JavaScript e pode ser estilizado através de CSS. Isso significa que podemos fornecer uma única imagem para o botão e usar JavaScript para personalizar o limite de cores e estilos CSS para indicar o estado do botão. Legal, não é?
Por fim, o SVG é apenas um documento XML, que pode ser editado com editores de texto e incorporado diretamente no HTML, tornando-o uma solução perfeita para criar componentes HTML reutilizáveis. Você está pronto para desenhar nosso botão?
Desenhando o botão com o Inkscape
O Inkscape é minha ferramenta favorita para criar gráficos vetoriais SVG. É gratuito e repleto de recursos poderosos, como uma grande coleção de predefinições de filtro integradas, rastreamento de bitmap e operações binárias de caminho. Comecei a usar o Inkscape para criar arte PCB, mas nos últimos dois anos, comecei a usá-lo para a maioria das minhas tarefas de edição gráfica.
Desenhar o botão no Inkscape é bastante simples. Vamos desenhar uma ilustração de vista superior do botão e seus quatro fios de metal que o conectam a outras partes, conforme segue:
- Retângulo cinza escuro de 12×12mm para a caixa de plástico, com cantos levemente arredondados para torná-la mais suave.
- Retângulo cinza claro menor, 10,5×10,5 para a tampa de metal.
- Quatro círculos mais escuros, um em cada canto para os pinos que prendem o botão.
- Um grande círculo no meio, que é o contorno da tampa do botão.
- Um círculo menor no meio para a parte superior da tampa do botão.
- Quatro retângulos cinza claro em forma de “T” para as pontas de metal do botão.
E o resultado, ligeiramente ampliado:
Como toque final, adicionaremos um pouco de magia de gradiente SVG ao contorno do botão, para dar uma sensação 3D:
Aqui vamos nós! Temos o visual, agora precisamos colocá-lo na web.
Do Inkscape para Web SVG
Como mencionei acima, os SVGs são bastante simples de incorporar em HTML - você pode simplesmente colar o conteúdo do arquivo SVG em seu documento HTML, abri-lo em um navegador e ele será renderizado na tela. Você pode vê-lo em ação no seguinte exemplo do CodePen:
No entanto, os arquivos SVG salvos do Inkscape contêm muita bagagem desnecessária, como a versão do Inkscape e a posição da janela quando você salvou o arquivo pela última vez. Em muitos casos, também existem elementos vazios, gradientes e filtros não utilizados, e todos eles aumentam o tamanho do arquivo e dificultam o trabalho com ele dentro do HTML.
Felizmente, o Inkscape pode limpar a maior parte da bagunça para nós. Aqui está como você faz isso:
- Vá para o menu Arquivo e clique em Limpar documento . (Isso removerá definições não utilizadas do seu documento.)
- Vá novamente para Arquivo e clique em Salvar como… . Ao salvar, selecione SVG otimizado ( *.svg ) na lista suspensa Salvar como tipo .
- Você verá uma caixa de diálogo "Saída SVG otimizada" com três guias. Marque todas as opções, exceto “Manter dados do editor”, “Manter definições não referenciadas” e “Preservar IDs criados manualmente…”.
A remoção de todas essas coisas criará um arquivo SVG menor com o qual é mais fácil trabalhar. No meu caso, o arquivo passou de 4593 bytes para apenas 2080 bytes, menos da metade do tamanho. Para arquivos SVG mais complexos, isso pode ser uma grande economia de largura de banda e pode fazer uma diferença notável no tempo de carregamento de sua página da web.
O SVG otimizado também é muito mais fácil de ler e entender. No trecho a seguir, você deve conseguir identificar facilmente os dois retângulos que formam o corpo do botão:
<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"/>
Você pode encurtar ainda mais o código, por exemplo, alterando a largura do traço do primeiro retângulo de 1.0003
para apenas 1
. Não faz uma diferença significativa no tamanho do arquivo, mas torna o código mais fácil de ler.
Em geral, uma passagem manual sobre o arquivo SVG gerado é sempre útil. Em muitos casos, você pode remover grupos vazios ou aplicar transformações de matriz, bem como simplificar as coordenadas de gradiente mapeando-as do “espaço do usuário em uso” (coordenadas globais) para a “caixa delimitadora do objeto” (relativa ao objeto). Essas otimizações são opcionais, mas você obtém um código mais fácil de entender e manter.
A partir deste ponto, vamos deixar o Inkscape de lado e trabalhar com a representação de texto da imagem SVG.
Criando um Web Component reutilizável
Até agora, temos os gráficos do nosso botão, prontos para serem inseridos em nosso simulador. Podemos personalizar facilmente a cor do botão alterando o atributo de fill
do círculo menor e a cor inicial do gradiente do círculo maior.
Nosso próximo objetivo é transformar nosso botão em um Web Component reutilizável que pode ser personalizado passando um atributo de color
e reage à interação do usuário (eventos de imprensa/liberação). Usaremos lit-element
, uma pequena biblioteca que simplifica a criação de Web Components.
lit-element
se destaca na criação de pequenas bibliotecas de componentes independentes. Ele é construído em cima do padrão Web Components, o que permite que esses componentes sejam consumidos por qualquer aplicação web, independentemente do framework usado: Angular, React, Vue ou Vanilla JS poderiam usar nosso componente.
A criação de componentes em lit-element
é feita usando uma sintaxe baseada em classe, com um método render()
que retorna o código HTML para o elemento. Um pouco semelhante ao React, se você estiver familiarizado com ele. No entanto, ao contrário do react, lit-element
usa literais de modelo com tags Javascript padrão para definir o conteúdo do componente.
Aqui está como você criaria um componente hello-world
simples:
import { customElement, html, LitElement } from 'lit-element'; @customElement('hello-world') export class HelloWorldElement extends LitElement { render() { return html` <h1> Hello, World! </h1> `; } }
Este componente pode ser usado em qualquer lugar em seu código HTML simplesmente escrevendo <hello-world></hello-world>
.
Nota : Na verdade, nosso botão requer um pouco mais de código: precisamos declarar uma propriedade de entrada para a cor, usando o @property()
(e com um valor padrão de vermelho) e colar o código SVG em nosso render()
, substituindo a cor da tampa do botão pelo valor da propriedade color (veja o exemplo). Os bits importantes estão na linha 5, onde definimos a propriedade color: @property() color = 'red';
Além disso, na linha 35 (onde usamos esta propriedade para definir a cor de preenchimento do círculo que forma a tampa do botão), usando a sintaxe literal do template JavaScript, escrita como ${color}
:
<circle cx="6" cy="6" r="2.9" fill="${color}" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08" />
Tornando-o interativo
A última peça do quebra-cabeça seria tornar o botão interativo. Há dois aspectos que precisamos considerar: a resposta visual à interação, bem como a resposta programática à interação.
Para a parte visual, podemos simplesmente inverter o preenchimento gradiente do contorno do botão, o que criará a ilusão de que o botão foi pressionado:
O gradiente para o contorno do botão é definido pelo seguinte código SVG, onde ${color}
é substituído pela cor do botão por lit-element
, conforme explicado acima:
<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>
Uma abordagem para a aparência do botão pressionado seria definir um segundo gradiente, inverter a ordem das cores e usá-lo como preenchimento do círculo sempre que o botão for pressionado. No entanto, existe um truque legal que nos permite reutilizar o mesmo gradiente: podemos girar o elemento svg em 180 graus usando uma transformação SVG:
<circle cx="6" cy="6" r="3.822" fill="url(#a)" transform="rotate(180 6 6)" />
O atributo transform
informa ao SVG que queremos girar o círculo em 180 graus e que a rotação deve ocorrer em torno do ponto (6, 6) que é o centro do círculo (definido por cx
e cy
). As transformações SVG também afetam o preenchimento da forma e, portanto, nosso gradiente também será girado.
Nós só queremos inverter o gradiente quando o botão é pressionado, então ao invés de adicionar o atributo transform
diretamente no elemento <circle>
, como fizemos acima, vamos definir uma classe CSS para este elemento, e então aproveitar do fato de que os atributos SVG podem ser definidos através de CSS, embora usando uma sintaxe ligeiramente diferente:
transform: rotate(180deg); transform-origin: 6px 6px;
Essas duas regras CSS fazem exatamente o mesmo que a transform
que fizemos acima - gire o círculo 180 graus em torno de seu centro em (6, 6). Queremos que essas regras sejam aplicadas apenas quando o botão for pressionado, então adicionaremos um nome de classe CSS ao nosso círculo:
<circle class="button-contour" cx="6" cy="6" r="3.822" fill="url(#a)" />
E agora podemos usar a pseudoclasse CSS :active para aplicar uma transformação ao button-contour
sempre que o elemento SVG for clicado:
svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; }
lit-element
nos permite anexar uma folha de estilo ao nosso componente declarando-o em um getter estático dentro de nossa classe de componente, usando um literal de modelo marcado:
static get styles() { return css` svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; } `; }
Assim como o modelo HTML, essa sintaxe nos permite injetar valores personalizados em nosso código CSS, mesmo que não precisemos disso aqui. lit-element
também cuida da criação do Shadow DOM para nosso componente, para que o CSS afete apenas os elementos dentro do nosso componente e não sangre para outras partes do aplicativo.
Agora, e o comportamento programático do botão quando pressionado? Queremos disparar um evento para que os usuários do nosso componente possam descobrir sempre que o estado do botão for alterado. Uma maneira de fazer isso é ouvir os eventos mousedown e mouseup no elemento SVG e disparar eventos “button-press”/“button-release” de forma correspondente. Isto é o que parece com a sintaxe lit-element
:
render() { const { color } = this; return html` <svg @mousedown=${() => this.dispatchEvent(new Event('button-press'))} @mouseup=${() => this.dispatchEvent(new Event('button-release'))} ... </svg> `; }
No entanto, esta não é a melhor solução, como veremos em breve. Mas primeiro, dê uma olhada rápida no código que temos até agora:
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> `; } }
- Veja demonstração →
Você pode clicar em cada um dos botões e ver como eles reagem. O vermelho ainda tem alguns ouvintes de eventos (definidos em index.html ), então quando você clicar nele você deverá ver algumas mensagens escritas no console. Mas espere, e se você quiser usar o teclado?
Tornando o componente acessível e compatível com dispositivos móveis
Viva! Criamos um componente de botão reutilizável com SVG e lit-element
!
Antes de assinarmos nosso trabalho, há algumas questões que devemos analisar. Primeiro, o botão não é acessível a pessoas que usam o teclado. Além disso, o comportamento no celular é inconsistente - os botões aparecem pressionados quando você segura o dedo sobre eles, mas os eventos JavaScript não são acionados se você segurar o dedo por mais de um segundo.
Vamos começar abordando o problema do teclado. Poderíamos tornar o botão acessível pelo teclado adicionando um atributo tabindex ao elemento svg, tornando-o focalizável. Uma alternativa melhor, na minha opinião, é apenas envolver o botão com um elemento padrão <button>
. Ao usar o elemento padrão, também fazemos com que ele funcione bem com leitores de tela e outras tecnologias assistivas.
Essa abordagem tem uma desvantagem, como você pode ver abaixo:
O elemento <button>
vem com algum estilo embutido. Isso pode ser facilmente corrigido aplicando algum CSS para remover esses 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; }
Observe que também substituímos o seletor que inverte a grade do contorno dos botões, usando button:active
no lugar de svg:active
. Isso garante que o estilo do botão pressionado seja aplicado sempre que o elemento <button>
real for pressionado, independentemente do dispositivo de entrada usado.
Podemos até tornar nosso componente mais amigável ao leitor de tela adicionando um atributo aria-label
que inclui a cor do botão:
<button aria-label="${color} pushbutton">
Ainda há mais uma coisa a resolver: os eventos de “pressionar o botão” e “soltar o botão”. Idealmente, queremos dispará-los com base na pseudo-classe CSS :active do botão, assim como fizemos no CSS acima. Em outras palavras, gostaríamos de acionar o evento “button-press” sempre que o botão se tornar :active
, e o evento “button-release” acionar sempre que for :not(:active)
.
Mas como você ouve uma pseudo-classe CSS do Javascript?
Acontece que não é tão simples. Fiz essa pergunta para a comunidade JavaScript Israel e, eventualmente, descobri uma ideia que funcionou no encadeamento sem fim: use o seletor :active
para acionar uma animação CSS super curta e, em seguida, posso ouvi-la do JavaScript usando o animationstart
evento.
Um rápido experimento CodePen provou que isso realmente funciona de forma confiável. Por mais que eu gostasse da sofisticação dessa ideia, decidi ir com uma solução diferente e mais simples. O evento animationstart
não está disponível no Edge e no iOS Safari, e acionar uma animação CSS apenas para detectar a alteração do estado do botão não parece ser a maneira correta de fazer as coisas.
Em vez disso, adicionaremos três pares de ouvintes de eventos ao elemento <button>
: mousedown/mouseup para o mouse, touchstart/touchend para dispositivos móveis e keyup/keydown para o teclado. Não é a solução mais elegante, na minha opinião, mas funciona em todos os 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()} >
Onde SPACE_KEY
é uma constante igual a 32, e up
/ down
são dois métodos de classe que despacham os eventos button-press
e 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')); } }
- Você pode encontrar o código-fonte completo aqui.
Conseguimos!
Foi uma jornada bastante longa que começou com o esboço dos requisitos e o desenho da ilustração para o botão no Inkscape, passou pela conversão do nosso arquivo SVG em um componente da web reutilizável usando lit-element
, e depois de nos certificarmos de que é acessível e compatível com dispositivos móveis, nós acabou com quase 100 linhas de código de um delicioso componente de botão virtual.
Este botão é apenas um único componente em uma biblioteca de código aberto de componentes eletrônicos virtuais que estou construindo. Você está convidado a espiar o código-fonte, ou conferir o Storybook online onde você pode ver e interagir com todos os componentes disponíveis.
E, finalmente, se você estiver interessado em Arduino, dê uma olhada no curso de programação Simon for Arduino que estou construindo atualmente, onde você também pode ver o botão em ação.
Até a próxima, então!