Keyboard Layout Editor JSONの使い方

この記事はキーボード #2 Advent Calendar 2020の8日目の記事です。

昨日は@F_YUUCHIさんの2020年にやったこと/できなかったこと&これからの話でした。

adventar.org

TL;DR

  • キーボードの配列を表現するのに Keyboard Layout Editorで使われているデータ形式が便利です
  • この記事では、そのデータ形式について述べ、デシリアライズとレイアウトを行います
  • デシリアライズは ijprest/kle-serial を使用すると簡単に行なえます

概要

Keyboard Layout Editor(以下、KLE)ではキーの位置や角度を自由に移動させてレイアウトを検討することができます。

View post on imgur.com
imgur.com

レイアウトをJSONとしてダウンロードすることができます。 そのJSONをアップロードすればそのレイアウトを再度編集することができて便利です。

ユースケース例

PCB上にキーを自動で配置する

KiCadでキーを自動で配置したいと思ったときに、入力ファイルとしてKLE JSONを使うことができます。

View post on imgur.com
imgur.com

yskoht.hatenablog.com

タイピングゲームのキーボードをカスタマイズする

KLE JSONを変更・編集できれば、タイピングゲームのキーボードをユーザーの好きにカスタマイズできます。

View post on imgur.com
imgur.com

yskoht.hatenablog.com

データ形式

データ形式の仕様は以下で説明されています。

github.com

この記事ではいくつかの例を通してデータ形式の主要な部分を理解したいと思います。

配列

{
  ["a", "b"],
  ["c", "d"]
}

f:id:yskoht:20201205124437p:plain:w480

1つ目の配列は y = 0, 2つ目の配列は y = 1, ... と配列ごとに行がインクリメントされます。各配列の1つ目の要素は x = 0, 2つ目の要素は x = 1, ... と列がインクリメントされます。

各キーの x, y は以下のようになっています。

a: { y: 0, x: 0 }
b: { y: 0, x: 1 }
c: { y: 1, x: 0 }
d: { y: 1, x: 1 }

位置( x, y )

{
  ["a", {x:1,y:0.5}, "b"],
  ["c", {y:-0.5},"d", {y:1},"e"],
  ["f"]
}

f:id:yskoht:20201205124335p:plain:w480

x, y に指定した値を、現在の x, y に足すことができます。

各キーの x, y は以下のようになっています。 x の値は要素ごとにインクリメントされ、配列ごとにリセットされます。 y の値は配列ごとにインクリメントされ、状態は後続のキーに保持されます。

a: { y: 0, x: 0 }
b: { y: 0.5, x: 2 } # 元の値 { y: 0, x: 1 } にオプションに指定した値が足されている
c: { y: 1.5, x: 0 } # y はインクリメントされる。x は0にリセットされる
d: { y: 1, x: 1 }   # y -= 0.5
e: { y: 2, x: 2 }   # y += 1
f: { y: 3, x: 0 }

幅/高さ ( w, h )

{
  ["a", {w:1.5,h:1.5}, "b"],
  [{w:2,h:2}, "c", {h:2}, "d"],
  ["e"]
}

f:id:yskoht:20201205131707p:plain:w480

キーの幅/高さを指定できます。xには wが加算されるので、隣のキーとは重なりませんが、 yはそのままなので上下のキーと重なってしまいます。 重ならないようにするにはyを指定する必要があります。

oddly-shaped keys ( x2, y2, w2, h2 )

ISO Enterのようなキーを表現できます。

{
  [{x:0.25,w:1.25,h:2,w2:1.5,h2:1,x2:-0.25},""]
}

f:id:yskoht:20201205133101p:plain:w480

これは以下のように、赤と青の2つのキーの重なりで表現されています。

f:id:yskoht:20201205133509p:plain

赤のキーは x:0.25, w:1.25, h:2が指定されており、青のキーは w2:1.5, h2:1, x2:-0.25が指定されています。青のキーに x2:-0.25 が指定されているのは x:0.25 で加えられたオフセットを戻すためです。

角度 ( r, rx, ry )

※この項目はドキュメントに書かれてないので、自分で調べた範囲のことを書いています。

キーの傾きを表現します。

{
  [{r:15},""]
}

f:id:yskoht:20201205134355p:plain:w480

r でキーの角度(度)を指定します。 rx, ry は回転の原点です。 transform-origin: rx, ry;を指定し、transform: rotate(r); することを想定されているように思います。

transform-origin - CSS: カスケーディングスタイルシート | MDN

f:id:yskoht:20201205141047p:plain:w480

以下のような仕様があるようです。

  • rx, ry はデフォルト 0
  • rは各配列の先頭にしか指定できない
  • r, rx, ryは後続のキーにも影響する
  • rx または ryが指定されると xrxyry にリセットされる

デシリアライズ

KLE JSONをデシリアライズし、各キーのx, y, r, rx, ry, w, hなどを求めます。

オープンソースライブラリを使う

以下のライブラリを使うとデシリアライズが簡単にできます

github.com

const kle = require('@ijprest/kle-serial')

const keyboard = kle.Serial.deserialize([
  ["a", "b"],
])

console.log(JSON.stringify(keyboard))

出力

{
  "meta": {
    "author": "",
    "backcolor": "#eeeeee",
    "background": null,
    "name": "",
    "notes": "",
    "radii": "",
    "switchBrand": "",
    "switchMount": "",
    "switchType": ""
  },
  "keys": [
    {
      "color": "#cccccc",
      "labels": [
        "a"
      ],
      "textColor": [],
      "textSize": [],
      "default": {
        "textColor": "#000000",
        "textSize": 3
      },
      "x": 0,
      "y": 0,
      "width": 1,
      "height": 1,
      "x2": 0,
      "y2": 0,
      "width2": 1,
      "height2": 1,
      "rotation_x": 0,
      "rotation_y": 0,
      "rotation_angle": 0,
      "decal": false,
      "ghost": false,
      "stepped": false,
      "nub": false,
      "profile": "",
      "sm": "",
      "sb": "",
      "st": ""
    },
    {
      "color": "#cccccc",
      "labels": [
        "b"
      ],
      "textColor": [],
      "textSize": [],
      "default": {
        "textColor": "#000000",
        "textSize": 3
      },
      "x": 1,
      "y": 0,
      "width": 1,
      "height": 1,
      "x2": 0,
      "y2": 0,
      "width2": 1,
      "height2": 1,
      "rotation_x": 0,
      "rotation_y": 0,
      "rotation_angle": 0,
      "decal": false,
      "ghost": false,
      "stepped": false,
      "nub": false,
      "profile": "",
      "sm": "",
      "sb": "",
      "st": ""
    }
  ]
}

現状だとrx, ry が指定されたときに、x, yrx, ryが設定されないようで、以下のPRが出ています。

github.com

自分でデシリアライズしてみる

KiCadのプラグインを作るときに自作したものを貼っておきます。

出力

{'x': 0, 'y': 0, 'w': 1, 'h': 1, 'x2': 0, 'y2': 0, 'w2': 1, 'h2': 1, 'r': 0, 'rx': 0, 'ry': 0, 'label': ['a']}
{'x': 1, 'y': 0, 'w': 1, 'h': 1, 'x2': 0, 'y2': 0, 'w2': 1, 'h2': 1, 'r': 0, 'rx': 0, 'ry': 0, 'label': ['b']}
{'x': 0, 'y': 1, 'w': 1, 'h': 1, 'x2': 0, 'y2': 0, 'w2': 1, 'h2': 1, 'r': 0, 'rx': 0, 'ry': 0, 'label': ['c']}
{'x': 1, 'y': 1, 'w': 1, 'h': 1, 'x2': 0, 'y2': 0, 'w2': 1, 'h2': 1, 'r': 0, 'rx': 0, 'ry': 0, 'label': ['d']}

レイアウト

デシリアライズにより各キーの位置関係と角度がわかったら、実際に描画したくなると思います。

雑にHTMLを出力してみます。ラッパーでtransform: rotate してから left, topで位置を指定します。

const kle = require('@ijprest/kle-serial')

// ErgoDox
const keyboard = kle.Serial.deserialize([
  [{x:3.5},"#\n3",{x:10.5},"*\n8"],
  [{y:-0.875,x:2.5},"@\n2",{x:1},"$\n4",{x:8.5},"&\n7",{x:1},"(\n9"],
  [{y:-0.875,x:5.5},"%\n5",{a:7},"",{x:4.5},"",{a:4},"^\n6"],
  [{y:-0.875,a:7,w:1.5},"",{a:4},"!\n1",{x:14.5},")\n0",{a:7,w:1.5},""],
  [{y:-0.375,x:3.5,a:4},"E",{x:10.5},"I"],
  [{y:-0.875,x:2.5},"W",{x:1},"R",{x:8.5},"U",{x:1},"O"],
  [{y:-0.875,x:5.5},"T",{a:7,h:1.5},"",{x:4.5,h:1.5},"",{a:4},"Y"],
  [{y:-0.875,a:7,w:1.5},"",{a:4},"Q",{x:14.5},"P",{a:7,w:1.5},""],
  [{y:-0.375,x:3.5,a:4},"D",{x:10.5},"K"],
  [{y:-0.875,x:2.5},"S",{x:1},"F",{x:8.5},"J",{x:1},"L"],
  [{y:-0.875,x:5.5},"G",{x:6.5},"H"],
  [{y:-0.875,a:7,w:1.5},"",{a:4},"A",{x:14.5},":\n;",{a:7,w:1.5},""],
  [{y:-0.625,x:6.5,h:1.5},"",{x:4.5,h:1.5},""],
  [{y:-0.75,x:3.5,a:4},"C",{x:10.5},"<\n,"],
  [{y:-0.875,x:2.5},"X",{x:1},"V",{x:8.5},"M",{x:1},">\n."],
  [{y:-0.875,x:5.5},"B",{x:6.5},"N"],
  [{y:-0.875,a:7,w:1.5},"",{a:4},"Z",{x:14.5},"?\n/",{a:7,w:1.5},""],
  [{y:-0.375,x:3.5},"",{x:10.5},""],
  [{y:-0.875,x:2.5},"",{x:1},"",{x:8.5},"",{x:1},""],
  [{y:-0.75,x:0.5},"","",{x:14.5},"",""],
  [{r:30,rx:6.5,ry:4.25,y:-1,x:1},"",""],
  [{h:2},"",{h:2},"",""],
  [{x:2},""],
  [{r:-30,rx:13,y:-1,x:-3},"",""],
  [{x:-3},"",{h:2},"",{h:2},""],
  [{x:-3},""]
])

console.log('<div style="position: relative;">')

keyboard.keys.forEach(key => {
  const coef = 50

  let wrapperStyle = ''
  wrapperStyle += 'position: absolute;'
  wrapperStyle += `transform: rotate(${key.rotation_angle}deg);`
  wrapperStyle += `transform-origin: ${key.rotation_x * coef}px ${key.rotation_y * coef}px;`

  let innerStyle = ''
  innerStyle += 'border-style: solid;'
  innerStyle += 'border-width: 1px;'
  innerStyle += 'position: absolute;'
  innerStyle += `left: ${key.x * coef}px;`
  innerStyle += `top: ${key.y * coef}px;`
  innerStyle += `width: ${key.width * coef - 4}px;`
  innerStyle += `height: ${key.height * coef - 4}px;`

  const label = key.labels[0] || ''

  console.log(`<div style="${wrapperStyle}">`)
  console.log(`  <div style="${innerStyle}">${label}</div>`)
  console.log(`</div>`)
})

console.log('</div>')

結果

f:id:yskoht:20201206001808p:plain

最後に

キーボード関連のツールを作ろうと思ったときに、この記事がなにかの役に立てれば幸いです。

この記事はpakbd rev3で書かれました。

参考