glasses-ui について
glasses-uiは、SDKを使ってEven Realities G2グラス向けのUIを作るためのスキル資料です。テキスト、リスト、グレースケール画像の画面を実装する開発者に、レイアウト制約や更新方法、操作パターンを説明します。
主な機能
- 01576×288ピクセルのキャンバスと階調規則
- 02テキスト・リスト・画像コンテナの制約
- 03ページ作成・再構築・テキスト更新
- 04画像データ転送と直列化の指針
- 05イベント取得とコンテナ重ね合わせ規則
- 06ページ分割と簡易コントロールのパターン
活用シーン
- 01G2グラス向けの複数行画面を設計
- 02画面を再描画せずにステータス文字を更新
- 03選択・スクロール可能なリストを表示
- 04入力取得レイヤーを備えたビットマップ画像の表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ even-realities/ everything-evenhub --skill glasses-uiインストール後、Claude Code で/glasses-uiを入力して読み込まれたか確認します。
詳細
Canvas
G2 グラスのディスプレイは 576x288 px のキャンバスで、次の特徴があります。
- 原点:左上隅(0, 0)
- X 軸:右方向に増加(0〜576)
- Y 軸:下方向に増加(0〜288)
- 色深度:4 ビットグレースケール — 16 段階の緑(値 0〜15)
- 白いピクセルは、ハードウェア上では明るい緑色で表示されます
- 黒いピクセルは消灯します(透明 — 現実世界が透けて見えます)
- 背景色や塗りつぶしはありません — キャンバスにデフォルトの背景はなく、描画されていない領域は透明です
コンテナーのルール
各ページはコンテナーで構成されます。ページごとに次の制限が適用されます。
- コンテナーは合計最大 12 個(すべての種類の合計)
- テキスト/リストコンテナーは最大 8 個
- 画像コンテナーは最大 4 個
isEventCapture: 1を持つコンテナーが必ず 1 個必要です — このコンテナーがユーザー入力イベントを受け取りますzOrderIndexによる重なり順(SDK 0.0.12 以降) — ページごとに、すべてのリスト/テキスト/画像コンテナーで一意のzOrderIndexを設定するか、どのコンテナーにも設定しないかのどちらかにしてください。値が大きいほど手前に表示されます。どこにも設定しない場合は、宣言順で重なり順が決まり、後に宣言されたものが上に表示されますcontainerIDはページごとに一意である必要があります(数値)containerNameはページごとに一意である必要があります(文字列、最大 16 文字)
共通プロパティ
すべてのコンテナータイプで、次のプロパティを共有します。
| プロパティ | 型 | 範囲 | 備考 |
|---|---|---|---|
xPosition | number | 0–576 | コンテナーの左端 |
yPosition | number | 0–288 | コンテナーの上端 |
width | number | 0–576 | コンテナーの幅 |
height | number | 0–288 | コンテナーの高さ(ピクセル) |
containerID | number | — | ページごとに一意 |
containerName | string | max 16 chars | ページごとに一意 |
isEventCapture | number | 0 or 1 | 1 に設定するコンテナーが必ず 1 個必要 |
zOrderIndex | number | — | 重なり順。値が大きいほど手前に表示されます。ページごとに設定するか省略するかを統一し、値は一意にしてください(SDK 0.0.12 以降) |
境界線のプロパティ
境界線のプロパティはテキストコンテナーとリストコンテナーにのみ適用されます(画像コンテナーには適用されません)。
| プロパティ | 型 | 範囲 | 備考 |
|---|---|---|---|
borderWidth | number | 0–5 | 0 = 境界線なし |
borderColor | number | 0–15 | グレースケールのレベル(0 = 黒、15 = 白/明るい緑) |
borderRadius | number | 0–10 | 角の丸みの半径 |
paddingLength | number | 0–32 | すべての辺に適用される均一なパディング |
TextContainerProperty
テキストコンテナーには、スクロール可能または固定のテキストコンテンツが表示されます。
| プロパティ | 型 | 範囲 | 備考 |
|---|---|---|---|
xPosition | number | 0–576 | 左端 |
yPosition | number | 0–288 | 上端 |
width | number | 0–576 | コンテナーの幅 |
height | number | 0–288 | コンテナーの高さ |
containerID | number | — | ページごとに一意 |
containerName | string | max 16 chars | ページごとに一意 |
isEventCapture | number | 0 or 1 | 1 に設定するコンテナーが必ず 1 個必要 |
zOrderIndex | number | — | 重なり順。値が大きいほど手前に表示されます(SDK 0.0.12 以降) |
borderWidth | number | 0–5 | 0 = 境界線なし |
borderColor | number | 0–15 | グレースケールのレベル |
borderRadius | number | 0–10 | 角の丸み |
paddingLength | number | 0–32 | 均一なパディング |
content | string | — | 表示するテキスト |
コンテンツの上限:
createStartUpPageContainer:最大 1000 文字textContainerUpgrade:最大 2000 文字rebuildPageContainer:最大 1000 文字
テキストの動作:
- テキストはコンテナーの幅に合わせて自動的に折り返されます
- 明示的な改行には
\nを使用します - 約 400〜500 文字で画面全体が埋まります
- ファームウェアのフォントセット内で Unicode がサポートされています
- テキストの配置オプションはありません(常に左揃え)
- フォントサイズは変更できません
- 太字や斜体のスタイルには対応していません
ListContainerProperty と ListItemContainerProperty
リストコンテナーは、ネイティブファームウェアが管理する、選択可能な項目のスクロール可能なリストを表示します。
ListItemContainerProperty はリスト項目を定義します。
| プロパティ | 型 | 範囲 | 備考 |
|---|---|---|---|
itemCount | number | 1–20 | リスト内の項目数 |
itemWidth | number | 0–576 | 項目の幅。0 = コンテナーの幅に合わせて自動調整 |
isItemSelectBorderEn | number | 0 or 1 | 選択枠のハイライトを有効化 |
itemName | string[] | 最大20項目、各項目最大64文字 | 項目ラベル文字列の配列 |
リストコンテナーの動作:
- ファームウェアがスクロールをネイティブに処理するため、手動でスクロールを実装する必要はありません
- リストはインプレースで更新できません。リストの内容を変更するには、ページ全体を再構築する必要があります
- 項目ごとのカスタムスタイルには対応していません
- 項目の高さは制御できません
- 項目間に区切り線はありません
ImageContainerProperty
画像コンテナーは、4ビットグレースケールのビットマップ画像を描画します。
| プロパティ | 型 | 範囲 | 備考 |
|---|---|---|---|
xPosition | number | 0–576 | 左端 |
yPosition | number | 0–288 | 上端 |
width | number | 20–288 | コンテナーの幅(ピクセル) |
height | number | 20–144 | コンテナーの高さ(ピクセル) |
containerID | number | — | ページごとに一意 |
containerName | string | 最大16文字 | ページごとに一意 |
zOrderIndex | number | — | 重なり順。値が大きいほど前面(SDK 0.0.12+) |
画像コンテナーは isEventCapture に対応していません。画像と組み合わせる場合は、イベントキャプチャレイヤーとしてテキストコンテナーを使用してください(下記の画像ベースのアプリのパターンを参照)。
画像データ形式:
- 受け付ける形式:
number[] | Uint8Array | ArrayBuffer | base64文字列 - 色深度:4ビットグレースケール(ピクセルあたり0~15の値)
前処理は任意です。 送信前にグレースケール化やディザリングを行う必要はありません。SDK が一般的な形式を内部で変換し、変換できない場合にのみ imageToGray4Failed を返します。線画、アイコン、QRコードは通常、そのままでも問題なく描画できます。写真やグラデーションは、16階調ディスプレイではクライアント側でコントラストを高め、Floyd–Steinberg ディザリングを適用すると見栄えがよくなりますが、まずはそのままの方法を試してください。
重要な動作:
- 画像コンテナーは作成時にはプレースホルダーであり、
updateImageRawDataが呼び出されるまで何も表示されません - 内容を表示するには、画像コンテナーの作成後に必ず
updateImageRawDataを呼び出してください - 画像データを同時に送信しないでください。画像の更新をキューに入れ、それぞれの完了を待ってから次の画像を送信してください
ページのライフサイクルメソッド
createStartUpPageContainer(container)
アプリの起動時に一度だけ呼び出され、初期ページのレイアウトを作成します。
createStartUpPageContainer(container: CreateStartUpPageContainer): Promise<StartUpPageCreateResult>
戻り値:
0— 成功1— 無効なパラメーター2— サイズ超過(コンテンツが大きすぎます)3— メモリ不足
rebuildPageContainer(container)
ページ全体を再描画します。レイアウト構造を変更する場合(コンテナーの追加・削除、コンテナーの種類の切り替え)に使用します。
rebuildPageContainer(container: RebuildPageContainer): Promise<boolean>
- 成功時は
trueを返します - ハードウェア上では一時的なちらつきが発生します(全体を再描画)
- テキストコンテンツの上限:テキストコンテナーあたり1000文字
textContainerUpgrade(container)
ページを再構築せずに、テキストコンテンツをインプレースで更新します。ちらつきは発生しません。
textContainerUpgrade(container: TextContainerUpgrade): Promise<boolean>
- 成功時は
trueを返します - ちらつきは発生しません。対象のテキストコンテナーのみを更新します
containerIDとcontainerNameは、既存のコンテナーと完全に一致する必要があります- テキストコンテンツの上限:2000文字
- コンテンツ全体を置き換えるには、
contentOffset: 0, contentLength: 0を使用します
updateImageRawData({ containerID, containerName, imageData })
画像コンテナーにピクセルデータを送信します。
updateImageRawData(params: {
containerID: number
containerName: string
imageData: number[] | Uint8Array | ArrayBuffer | string
}): Promise<string>
戻り値のステータス文字列:
'success'— 画像を表示しました'imageException'— 一般的な画像エラー'imageSizeInvalid'— 画像の寸法がコンテナーと一致しません'imageToGray4Failed'— 色変換に失敗しました'sendFailed'— 送信エラー
同時に呼び出さないでください。次の画像を送信する前に、前の呼び出しが完了するまで待ってください。
shutDownPageContainer(exitMode?)
現在のページを終了します。
shutDownPageContainer(exitMode?: number): Promise<boolean>
exitMode: 0— 確認なしですぐに終了しますexitMode: 1— 終了確認ダイアログを表示します
ベストプラクティス
- 頻繁な更新(カウンター、ステータス行、ライブデータフィード)には
textContainerUpgradeを使用します — ちらつきなく、その場で更新されます - レイアウトを変更する場合は
rebuildPageContainerを使用します — コンテナの追加・削除、コンテナタイプの切り替え、リスト項目の更新などが該当します textContainerUpgradeの呼び出し時は、containerIDとcontainerNameを必ず正確に一致させます — 不一致でもエラーは表示されず、処理に失敗しますupdateImageRawDataを並行して呼び出さないでください — 更新をキューに入れ、次の更新を送信する前にそれぞれを待機します- 長いテキストは約400~500文字ごとに事前にページ分割し、スクロールイベントで
rebuildPageContainerを使用します - BLE 経由の画像フレームは1枚あたり約0.5~2秒かかります — SDK 0.0.12以降では生データを内部でLZ4圧縮するため転送時間が短縮されますが、差分エンコーディングには対応していません。ターン制を前提に設計し、複数FPSを前提とするループは避けてください
- テキストの更新は画像の更新よりはるかに高速です — 即時性が必要なものにはテキストを使用し、画像コンテナの更新は完了するまで待ちます
- 画像に限らず、すべてのブリッジ呼び出しを直列化します — 次の処理を開始する前に、それぞれを
awaitします。レンダリングとストレージの呼び出しを並行すると、接続がクラッシュすることがあります - BLE 呼び出しには呼び出しごとのタイムアウトを設定します — 不安定なホップが1つあるだけで約30秒停止することがあります。
Promise.raceで数秒の上限を設定してください - 永続状態への書き込みはデバウンスします —
setLocalStorageは同じ BLE リンクを共有するため、ティック/ページ切り替え時にデバウンスし、終了時にフラッシュします createStartUpPageContainerは必ず1回だけ呼び出します — 以降のすべてのレンダリングでは、rebuildPageContainerまたは*Upgrade呼び出しを使用します
一般的なUIパターン
偽のボタン
テキスト項目の先頭に > を付け、カーソルインジケーターとして使用すると、ボタンや選択状態をシミュレートできます。
> Start
Settings
Exit
選択ハイライト
個々のテキストコンテナの borderWidth を、0(未選択)と0以外の値(選択)の間で切り替えて、フォーカスを示します。rebuildPageContainer が必要です。
複数行レイアウト
複数のテキストコンテナを縦に積み重ねます。288pxのキャンバスに3つの等しい行を配置する場合:
// Row 1: y=0, height=96
// Row 2: y=96, height=96
// Row 3: y=192, height=96
プログレスバー
Unicodeのブロック文字を使用して、テキストコンテナ内にプログレスバーを描画します。
const filled = Math.round((progress / 100) * barWidth)
const bar = '━'.repeat(filled) + '─'.repeat(barWidth - filled)
content = `Progress: ${bar} ${progress}%`
ページ送り
コンテンツを約400~500文字ごとに事前にページ分割します。スクロール/ナビゲーションイベントで、新しいページの範囲を指定して rebuildPageContainer を呼び出します。
const pages = paginateText(fullText, 450)
let currentPage = 0
async function showPage(index: number) {
currentPage = index
await bridge.rebuildPageContainer({
containerTotalNum: 1,
textObject: [{ ...containerConfig, content: pages[index] }],
})
}
完全なコード例
import {
waitForEvenAppBridge,
type TextContainerProperty,
type ImageContainerProperty,
} from '@evenrealities/even_hub_sdk'
const bridge = await waitForEvenAppBridge()
// Create initial page with text and image containers
const textContainer: TextContainerProperty = {
xPosition: 0, yPosition: 0, width: 576, height: 200,
borderWidth: 0, borderColor: 5, paddingLength: 4,
containerID: 1, containerName: 'main',
content: 'Hello from G2!',
isEventCapture: 1,
}
const imageContainer: ImageContainerProperty = {
xPosition: 200, yPosition: 210, width: 100, height: 60,
containerID: 2, containerName: 'icon',
}
const result = await bridge.createStartUpPageContainer({
containerTotalNum: 2,
textObject: [textContainer],
imageObject: [imageContainer],
})
if (result === 0) {
// Update image content after creation
await bridge.updateImageRawData({
containerID: 2,
containerName: 'icon',
imageData: [/* pixel data */],
})
// Flicker-free text update
await bridge.textContainerUpgrade({
containerID: 1,
containerName: 'main',
content: 'Updated text!',
contentOffset: 0,
contentLength: 0,
})
}
画像ベースのアプリパターン
画像を中心とするアプリ(キャンバスやビットマップを主な表示要素として描画するアプリなど)を構築する場合は、画像コンテナの背後に、イベントを取得するレイヤーとして全画面のテキストコンテナを使用します。
// Full-screen transparent text container — receives events, invisible to user
const eventLayer: TextContainerProperty = {
xPosition: 0, yPosition: 0, width: 576, height: 288,
containerID: 1, containerName: 'eventLayer',
content: ' ', // single space — required, cannot be empty
isEventCapture: 1, // this layer catches all input events
borderWidth: 0, borderColor: 0, paddingLength: 0,
zOrderIndex: 1, // back layer (SDK 0.0.12+; if one container sets zOrderIndex, all must)
}
// Image container renders on top of the event layer
const imageLayer: ImageContainerProperty = {
xPosition: 0, yPosition: 0, width: 200, height: 100,
containerID: 2, containerName: 'display',
zOrderIndex: 2, // front layer — larger = closer to front
// isEventCapture: 0 (default) — image containers do not capture events
}
await bridge.createStartUpPageContainer({
containerTotalNum: 2,
textObject: [eventLayer],
imageObject: [imageLayer],
})
// Always send image data after creation
await bridge.updateImageRawData({
containerID: 2,
containerName: 'display',
imageData: pixelData,
})
テキストコンテナがイベントを受け取り、画像コンテナがその上に描画されます。0.0.12より前のSDKでは、両方の zOrderIndex フィールドを削除し、宣言順(後に宣言されたものが上に描画される)を利用してください。
タスク
$ARGUMENTS