使用 SVG 和 <lit-element> 重新創建 Arduino 按鈕
已發表: 2022-03-10在本文中,您將學習如何構建模仿物理對象的自定義 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 相對簡單,因為它只有兩種輸出狀態:開和關。 在幕後,它使用 SVG 過濾器來創建照明效果。
按鈕更有趣。 它也有兩種狀態,但它必須對用戶輸入做出反應並相應地更新其狀態,這就是挑戰的來源,我們很快就會看到。 但首先,讓我們確定我們將要創建的組件的需求。
定義按鈕的要求
我們的組件將類似於一個 12 毫米的按鈕。 這些按鈕在電子入門套件中非常常見,並帶有多種顏色的蓋子,如下圖所示:
在行為方面,按鈕應該有兩種狀態:按下和釋放。 這些類似於 mousedown/mouseup HTML 事件,但我們必須確保按鈕也可以在移動設備上使用,並且可供沒有鼠標的用戶訪問。
由於我們將使用按鈕的狀態作為 Arduino 的輸入,因此無需支持“單擊”或“雙擊”事件。 由模擬中運行的 Arduino 程序決定如何對按鈕的狀態進行操作,物理按鈕不會產生點擊事件。
如果您想了解更多信息,請查看我與 Benjamin Gruenbaum 於 2019 年在 SmashingConf Freiburg 舉行的演講:“點擊剖析”。
總結一下我們的要求,我們的按鈕需要:
- 看起來類似於物理 12 毫米按鈕;
- 有兩種不同的狀態:按下和釋放,它們應該是視覺可辨的;
- 支持鼠標交互、移動設備和鍵盤用戶可以訪問;
- 支持不同的帽子顏色(至少紅色、綠色、藍色、黃色、白色和黑色)。
現在我們已經定義了需求,我們可以開始著手實現了。
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 中繪製按鈕非常簡單。 我們將繪製按鈕及其連接到其他部分的四根金屬引線的俯視圖,如下所示:
- 塑料外殼為 12×12mm 深灰色矩形,邊角略圓,使其更柔軟。
- 較小的 10.5×10.5 淺灰色矩形金屬蓋。
- 四個較暗的圓圈,每個角落一個用於將按鈕固定在一起的引腳。
- 中間一個大圓圈,就是扣帽的輪廓。
- 按鈕帽頂部中間的一個較小的圓圈。
- 四個“T”形的淺灰色矩形用於按鈕的金屬引線。
結果,稍微放大:
最後,我們將在按鈕的輪廓上添加一些 SVG 漸變魔法,使其具有 3D 感覺:
我們去吧! 我們有了視覺效果,現在我們需要把它放到網絡上。
從 Inkscape 到 Web SVG
正如我上面提到的,SVG 嵌入 HTML 非常簡單——您只需將 SVG 文件的內容粘貼到 HTML 文檔中,在瀏覽器中打開它,它就會呈現在您的屏幕上。 您可以在以下 CodePen 示例中看到它的實際效果:
但是,從 Inkscape 保存的 SVG 文件包含許多不必要的包袱,例如 Inkscape 版本和上次保存文件時的窗口位置。 在許多情況下,還有空元素、未使用的漸變和過濾器,它們都會增大文件大小,並使其在 HTML 中更難使用。
幸運的是,Inkscape 可以為我們清理大部分的爛攤子。 這是您的操作方法:
- 轉到“文件”菜單,然後單擊“清理文檔” 。 (這將從您的文檔中刪除未使用的定義。)
- 再次轉到文件並單擊另存為...。 保存時,在Save as type下拉菜單中選擇Optimized SVG ( *.svg )。
- 您將看到一個帶有三個選項卡的“優化的 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 度,並且旋轉應該圍繞圓心(由cx
和cy
定義)的點 (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>
元素帶有一些內置樣式。 這可以通過應用一些 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-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 中概述需求和繪製按鈕插圖開始,使用lit-element
將我們的 SVG 文件轉換為可重用的 Web 組件,在確保它可訪問且對移動設備友好之後,我們最終得到了一個令人愉快的虛擬按鈕組件的近 100 行代碼。
這個按鈕只是我正在構建的虛擬電子組件開源庫中的一個組件。 邀請您查看源代碼,或查看在線故事書,您可以在其中查看所有可用組件並與之交互。
最後,如果您對 Arduino 感興趣,請查看我目前正在為 Arduino 編程的 Simon 課程,您還可以在其中看到按鈕的運行情況。
那就等下次吧!