SVGと<lit-element>を使用したArduinoプッシュボタンの再作成
公開: 2022-03-10この記事では、Arduinoプッシュボタンなどの物理オブジェクトを模倣するカスタムHTMLコンポーネントを構築する方法を学習します。 Inkscapeでコンポーネントを最初から描画し、生成されたSVGコードをWeb用に最適化し、軽量の
lit-element
ライブラリを使用してスタンドアロンのWebコンポーネントとしてラップし、アクセシビリティとモバイルユーザビリティの考慮事項に特に注意を払います。 今日は、Arduinoや電子プロジェクトで一般的に使用される瞬間的なプッシュボタンコンポーネントを模倣するHTMLコンポーネントを作成する旅を紹介します。 SVG、Webコンポーネント、 lit-element
などのテクノロジーを使用し、JavaScript-CSSのトリックを使用してボタンにアクセスできるようにする方法を学習します。
はじめましょう!
ArduinoからHTMLへ:プッシュボタンコンポーネントの必要性
旅に出る前に、私たちが何を作成しようとしているのか、そしてもっと重要なのはその理由を探りましょう。 私はavr8jsと呼ばれるJavaScriptでオープンソースのArduinoシミュレーターを作成しています。 このシミュレーターはArduinoコードを実行することができ、Arduinoのプログラミング方法をメーカーに教える一連のチュートリアルとコースで使用します。
シミュレータ自体はプログラムの実行のみを処理します—命令ごとにコードを実行し、プログラムロジックに従って内部状態とメモリバッファを更新します。 Arduinoプログラムと対話するには、シミュレーターに入力を送信したり、その出力に反応したりできる仮想電子コンポーネントを作成する必要があります。
シミュレーターを単独で実行することは、JavaScriptを単独で実行することに非常によく似ています。 いくつかのHTML要素も作成し、それらをDOMを介してJavaScriptコードにフックしない限り、ユーザーと実際に対話することはできません。
したがって、プロセッサのシミュレータに加えて、物理ハードウェアを模倣するHTMLコンポーネントのライブラリにも取り組んでいます。これは、ほとんどすべての電子プロジェクトに見られる最初の2つのコンポーネントであるLEDとプッシュボタンから始まります。
LEDは、オンとオフの2つの出力状態しかないため、比較的単純です。 舞台裏では、SVGフィルターを使用して照明効果を作成します。
押しボタンはもっと面白いです。 また、2つの状態がありますが、ユーザーの入力に反応し、それに応じて状態を更新する必要があります。これから、後で説明するように、課題が発生します。 ただし、最初に、作成するコンポーネントから要件を特定しましょう。
押ボタンの要件の定義
私たちのコンポーネントは12mmの押しボタンに似ています。 これらのボタンは、電子機器のスターターキットで非常に一般的であり、下の写真に示すように、複数の色のキャップが付属しています。
動作に関しては、プッシュボタンには、押された状態と離された状態の2つの状態が必要です。 これらはmousedown / mouseup HTMLイベントに似ていますが、プッシュボタンがモバイルデバイスからも使用でき、マウスを持たないユーザーがアクセスできることを確認する必要があります。
プッシュボタンの状態をArduinoの入力として使用するため、「クリック」または「ダブルクリック」イベントをサポートする必要はありません。 ボタンの状態に基づいて動作する方法を決定するのはシミュレーションで実行されているArduinoプログラム次第であり、物理的なボタンはクリックイベントを生成しません。
詳細については、2019年にSmashingConfFreiburgでBenjaminGruenbaumと行った講演「AnatomyofaClick」をご覧ください。
要件を要約すると、プッシュボタンは次のことを行う必要があります。
- 物理的な12mmプッシュボタンに似ています。
- 押された状態と解放された状態の2つの異なる状態があり、視覚的に識別できる必要があります。
- マウスインタラクション、モバイルデバイスをサポートし、キーボードユーザーがアクセスできます。
- さまざまなキャップの色(少なくとも赤、緑、青、黄色、白、黒)をサポートします。
要件を定義したので、実装の作業を開始できます。
勝利のためのSVG
ほとんどのWebコンポーネントは、CSSとHTMLの組み合わせを使用して実装されます。 より複雑なグラフィックが必要な場合は、通常、JPGまたはPNG形式(または懐かしいと感じる場合はGIF)のラスター画像を使用します。
ただし、この場合は、SVGグラフィックという別のアプローチを使用します。 SVGは、CSSよりもはるかに簡単に複雑なグラフィックスに適しています(確かに、CSSを使用して魅力的なものを作成できますが、そうする必要があるという意味ではありません)。 しかし、心配しないでください。CSSを完全に諦めているわけではありません。 プッシュボタンのスタイリング、そして最終的にはそれらにアクセスできるようにするのに役立ちます。
SVGには、ラスターグラフィックス画像と比較して、もう1つの大きな利点があります。JavaScriptからの操作が非常に簡単で、CSSを介してスタイルを設定できます。 これは、ボタンに単一の画像を提供し、JavaScriptを使用してカラーキャップをカスタマイズし、CSSスタイルを使用してボタンの状態を示すことができることを意味します。 きちんとね。
最後に、SVGは単なるXMLドキュメントであり、テキストエディターで編集したり、HTMLに直接埋め込んだりできるため、再利用可能なHTMLコンポーネントを作成するための完璧なソリューションになります。 プッシュボタンを描画する準備はできていますか?
Inkscapeでプッシュボタンを描画する
Inkscapeは、SVGベクターグラフィックを作成するための私のお気に入りのツールです。 これは無料で、組み込みのフィルタープリセットの大規模なコレクション、ビットマップトレース、パスの2項演算などの強力な機能が満載です。 私はPCBアートの作成にInkscapeを使い始めましたが、過去2年間で、ほとんどのグラフィック編集タスクにInkscapeを使い始めました。
Inkscapeでプッシュボタンを描画するのは非常に簡単です。 次のように、ボタンと、ボタンを他の部品に接続する4本の金属リードの上面図を描画します。
- プラスチックケース用の12×12mmの濃い灰色の長方形で、角がわずかに丸みを帯びて柔らかくなっています。
- 金属カバー用の小さい10.5×10.5の薄い灰色の長方形。
- ボタンを一緒に保持するピン用に各コーナーに1つずつ、4つの暗い円。
- 真ん中の大きな円、つまりボタンキャップの輪郭です。
- ボタンキャップの上部の中央にある小さな円。
- ボタンの金属リード用の「T」字型の4つの薄い灰色の長方形。
そして結果は、わずかに拡大されました:
最後の仕上げとして、ボタンの輪郭にSVGグラデーションの魔法を追加して、3D感を与えます。
どうぞ! ビジュアルができたので、それをWebに取り込む必要があります。
InkscapeからWebSVGへ
上で述べたように、SVGはHTMLに埋め込むのが非常に簡単です。SVGファイルのコンテンツをHTMLドキュメントに貼り付け、ブラウザで開くだけで、画面にレンダリングされます。 次のCodePenの例で実際の動作を確認できます。
ただし、Inkscapeから保存されたSVGファイルには、Inkscapeのバージョンや、ファイルを最後に保存したときのウィンドウの位置など、不要な手荷物がたくさん含まれています。 多くの場合、空の要素、未使用のグラデーション、フィルターもあり、それらはすべてファイルサイズを肥大化させ、HTML内での操作を困難にします。
幸いなことに、Inkscapeは私たちのためにほとんどの混乱をきれいにすることができます。 これがあなたのやり方です:
- [ファイル]メニューに移動し、[ドキュメントのクリーンアップ]をクリックします。 (これにより、未使用の定義がドキュメントから削除されます。)
- もう一度[ファイル]に移動し、[名前を付けて保存... ]をクリックします。 保存するときは、[ファイルの種類]ドロップダウンで[最適化されたSVG ( * .svg )]を選択します。
- 3つのタブがある「最適化されたSVG出力」ダイアログが表示されます。 「エディタデータを保持する」、「参照されていない定義を保持する」、「手動で作成されたIDを保持する…」を除くすべてのオプションをチェックします。
これらすべてを削除すると、操作しやすい小さなSVGファイルが作成されます。 私の場合、ファイルは4593バイトから2080バイトになり、サイズは半分以下になりました。 より複雑なSVGファイルの場合、これは帯域幅の大幅な節約になり、Webページの読み込み時間に顕著な違いをもたらす可能性があります。
最適化されたSVGは、読みやすく、理解しやすいものです。 次の抜粋では、プッシュボタンの本体を構成する2つの長方形を簡単に見つけることができるはずです。
<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
属性を渡すことでカスタマイズでき、ユーザーの操作(プレス/リリースイベント)に反応します。 Webコンポーネントの作成を簡素化する小さなライブラリであるlit-element
を使用します。
lit-element
は、小さなスタンドアロンコンポーネントライブラリの作成に優れています。 これはWebコンポーネント標準に基づいて構築されているため、使用するフレームワークに関係なく、これらのコンポーネントを任意のWebアプリケーションで使用できます。Angular、React、Vue、VanillaJSはすべてこのコンポーネントを使用できます。
lit-element
でのコンポーネントの作成は、要素のHTMLコードを返すrender()
メソッドを使用して、クラスベースの構文を使用して行われます。 あなたがそれに精通しているなら、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()
デコレータを使用して(デフォルト値は赤で)、色の入力プロパティを宣言し、SVGコードをrender()
に貼り付ける必要があります。 render()
メソッド。ボタンキャップの色をcolorプロパティの値に置き換えます(例を参照)。 重要なビットは5行目にあり、colorプロパティを定義しています。 @property() color = 'red';
また、35行目(このプロパティを使用して、ボタンのキャップを構成する円の塗りつぶしの色を定義します)で、JavaScriptテンプレートのリテラル構文を使用して、 ${color}
と記述します。
<circle cx="6" cy="6" r="2.9" fill="${color}" stroke="#2f2f2f" stroke-opacity=".47" stroke-width=".08" />
インタラクティブにする
パズルの最後のピースは、ボタンをインタラクティブにすることです。 考慮する必要のある2つの側面があります。相互作用に対する視覚的応答と、相互作用に対するプログラム的応答です。
視覚的な部分では、ボタンの輪郭のグラデーションの塗りつぶしを単純に逆にすることができます。これにより、ボタンが押されたような錯覚が作成されます。
ボタンの輪郭のグラデーションは、次の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>
押されたボタンの外観の1つのアプローチは、2番目のグラデーションを定義し、色の順序を逆にして、ボタンが押されるたびにそれを円の塗りつぶしとして使用することです。 ただし、同じグラデーションを再利用できる優れたトリックがあります。SVG変換を使用してsvg要素を180度回転させることができます。
<circle cx="6" cy="6" r="3.822" fill="url(#a)" transform="rotate(180 6 6)" />
transform
属性は、円を180度回転させ、円の中心( cx
とcy
で定義)である点(6、6)を中心に回転させる必要があることをSVGに通知します。 SVG変換はシェイプの塗りつぶしにも影響するため、グラデーションも回転します。
ボタンが押されたときにのみグラデーションを反転したいので、上記のように<circle>
要素に直接transform
属性を追加する代わりに、実際にこの要素にCSSクラスを設定してから、これを利用します。構文は少し異なりますが、SVG属性はCSSを介して設定できるという事実について:
transform: rotate(180deg); transform-origin: 6px 6px;
これらの2つの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
を使用すると、タグ付きテンプレートリテラルを使用して、コンポーネントクラス内の静的ゲッターでスタイルシートを宣言することにより、スタイルシートをコンポーネントにアタッチできます。
static get styles() { return css` svg:active .button-contour { transform: rotate(180deg); transform-origin: 6px 6px; } `; }
HTMLテンプレートと同様に、この構文を使用すると、ここでは必要ありませんが、CSSコードにカスタム値を挿入できます。 lit-element
は、コンポーネントのShadow DOMの作成も処理するため、CSSはコンポーネント内の要素にのみ影響し、アプリケーションの他の部分にブリードしません。
では、ボタンを押したときのプログラム上の動作についてはどうでしょうか。 ボタンの状態が変化するたびにコンポーネントのユーザーが把握できるように、イベントを発生させたいと考えています。 これを行う1つの方法は、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
を使用して再利用可能なプッシュボタンコンポーネントを作成しました!
作業を承認する前に、確認する必要のある問題がいくつかあります。 まず、キーボードを使用する人はボタンにアクセスできません。 さらに、モバイルでの動作に一貫性がありません。ボタンを指で押したままにするとボタンが押されたように見えますが、指を1秒以上押し続けてもJavaScriptイベントは発生しません。
キーボードの問題に取り組むことから始めましょう。 svg要素にtabindex属性を追加して、ボタンをキーボードでアクセスできるようにし、フォーカス可能にすることができます。 私の意見では、より良い代替手段は、ボタンを標準の<button>
要素でラップすることです。 標準の要素を使用することで、スクリーンリーダーやその他の支援技術とうまく連携することもできます。
以下に示すように、このアプローチには1つの欠点があります。
<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; }
svg:active
の代わりにbutton:active
を使用して、ボタンの輪郭のグリッドを反転するセレクターも置き換えたことに注意してください。 これにより、使用する入力デバイスに関係なく、実際の<button>
要素が押されるたびにボタンが押されたスタイルが適用されます。
ボタンの色を含むaria-label
属性を追加することで、コンポーネントをよりスクリーンリーダーに適したものにすることもできます。
<button aria-label="${color} pushbutton">
取り組むべきことがまだもう1つあります。それは、「ボタンを押す」イベントと「ボタンを離す」イベントです。 理想的には、上記のCSSで行ったように、ボタンのCSS:active疑似クラスに基づいてそれらを起動する必要があります。 つまり、ボタンが:active
になると「button-press」イベントが発生し、 :not(:active)
になると「button-release」イベントが発生します。
しかし、JavascriptからCSS疑似クラスをどのように聞きますか?
結局のところ、それはそれほど単純ではありません。 私はJavaScriptIsraelコミュニティにこの質問をし、最終的には無限のスレッドから機能する1つのアイデアを掘り起こしました:active
セレクターを使用して超短CSSアニメーションをトリガーし、次に、 animationstart
を使用してJavaScriptからそれを聞くことができます。イベント。
簡単なCodePen実験により、これが実際に確実に機能することが証明されました。 このアイデアの洗練された点が気に入ったのと同じくらい、私は別のより単純なソリューションを採用することにしました。 AnimationstartイベントはEdgeとanimationstart
では利用できません。また、ボタンの状態の変化を検出するためだけにCSSアニメーションをトリガーすることは、正しい方法とは思えません。
代わりに、 <button>
要素に3組のイベントリスナーを追加します。マウスの場合は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
イベントをディスパッチする2つのクラスメソッドです。
@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を見てください。ここでは、プッシュボタンの動作も確認できます。
次回まで、それでは!