#d3で日本語配列キーボードを作ったお話し
##前書き(読み飛ばし推奨)
先日、javascriptの練習もかねてタイピングゲームを作ろうと思いました。
タイピングゲームといえば画面下部に表示されるキーボード。
1から自分で作るのはめんど…他の部分に時間を掛けたかったので、それっぽいものをネットで探したわけです。
型枠さえ見つかれば少しいじってjQuery等でフィードバックできるかなぁと思っていたのですが、よさげなものが見つからない。
というのも、あるにはあったのですが全て英語配列キーボードだったんです。
英語配列をちょちょっといじって日本語配列にしようとも思ったんですが、考えてみればCSSじゃ"エンターキー"が表示できない...。
エンターキーのみ画像にするのもかっこ悪いですし、泣く泣く自分で作ることとしました。
CSS+HTMLではエンターキーを表示できない問題が(自分の能力では)解決出来なかったので、d3.jsを用いてsvgとしてキーボードを作成しました。
(まぁ作成し終わった段階でそもそもエンターキーって必要だったのか?という自問自答をしたのはご愛敬)
せっかく頑張って作ったキーボードなので、ここでどうやって作ったか軽く解説しようと思います。
##ここから本題
###デモ
まぁ出来てるものを触ってからのほうが解説がわかりやすいと思いますので、まずはデモをご覧ください。
デモページ
(キーボードフィードバックを得るためには、一度キーボード部分をクリックしてからキーを押下してください)
##実装
###準備
まず下準備
結構長いですがやってることは全部ただの準備です。
// キーコード
var codes = [
[0, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 189, 222, 220, 8],
[0, 81, 87, 69, 82, 84, 89, 85, 73, 79, 80, 192, 219],
[0, 65, 83, 68, 70, 71, 72, 74, 75, 76, 187, 186, 221],
[16, 90, 88, 67, 86, 66, 78, 77, 188, 190, 191, 226, 16],
[17, 91, 18, 0, 32, 0, 18, 93, 17],
]
// キーの文字
var text = [
["F/H", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "^", "\\", "BS"],
["Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "@", "["],
["CapsLock", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", ":", "]"],
["Shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "\\", "Shift"],
["Ctrl", "Win", "Alt", "", "", "", "Alt", "", "Ctrl"],
];
// キーの文字(shift押下時)
var shiftText = [
["F/H", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "", "=", "~", "|", "BS"],
["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "`", "{"],
["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", "+", "*", "}"],
["Shift", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", "_", "Shift"],
["Ctrl", "Win", "Alt", "", "", "", "Alt", "", "Ctrl"],
]
// 上記3つの配列から、同じインデックスの要素同士をまとめた3次元配列を生成する
// 例) data[0][0] ==> ["F/H", "F/H", 0]
// data[0][1] ==> ["1", "!", 49]
var data = [
d3.zip(text[0], shiftText[0], codes[0]),
d3.zip(text[1], shiftText[1], codes[1]),
d3.zip(text[2], shiftText[2], codes[2]),
d3.zip(text[3], shiftText[3], codes[3]),
d3.zip(text[4], shiftText[4], codes[4]),
];
// キーの幅を定義
var keyWdith = {
w1: 40,
w2: 64,
w3: 76,
w4: 104,
w5: 220,
};
// キー幅を設定
var keyVariety = [
["w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1"],
["w2", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1"],
["w3", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1"],
["w4", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w1", "w3"],
["w2", "w2", "w2", "w1", "w5", "w1", "w2", "w1", "w2"],
];
// 文字の印字場所を設定
var textAlign = [
["l", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "r"],
["l", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n"],
["l", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n"],
["l", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "r"],
["l", "l", "l", "n", "r", "r", "r", "r", "r"],
]
// 文字の印字場所を設定
var tx = {
l: 5,
n: 20,
r: 40
}
// 文字の印字場所を設定
var ty = {
l: 35,
n: 27,
r: 35,
}
// 文字の印字場所を設定
var ta = {
l: "start",
n: "middle",
r: "end",
}
なにをしているかはコードを読んでもらえれば何となくわかると思いますが、軽く説明します。
キーコードや印字されている文字などは読んで字のごとくそのキーのデータを表します。
これを、d3で提供されているzip関数を使って、同じインデックス、つまり1つのキーについての各々のデータを1つのデータ(["文字", "文字", コード]
)としてまとめ、これを要素とする2次元配列とします。(配列を要素に持つので実際は3次元配列)
このキーごとのデータを各要素に束縛することで、shift押下時の処理やフィードバックを実装しやすくしています。
###要素の作成
以下でsvg要素を追加していきます。
ここが今回のコードで一番重要です。
// svg作成
var svg = d3.select("body").append("svg");
// キーボードの要素はこのg要素に格納
var keyboard = svg.append("g");
// キーボードを囲う枠
var frame = keyboard.append("rect");
// キーボードの各行を生成
var keyLines = keyboard.selectAll("g").data(data).enter().append("g");
// 各キー要素(この中にrectとtextを格納して1つのキーを表す)
var keys = keyLines.selectAll("g").data(function (d) { return d; }).enter().append("g");
// キーを表す四角形と、印字されている文字をg要素の中に生成
var keyButtons = keys.append("rect");
var texts = keys.append("text");
// エンターキーは特別に別途生成する
var enterKey = keyboard.append("g");
var enterButton = enterKey.append("path");
var enterText = enterKey.append("text").text("Enter");
この部分について解説します。
まずsvgを作成して、大元となるg要素を追加します。
// svg作成
var svg = d3.select("body").append("svg");
// キーボードの要素はこのg要素に格納
var keyboard = svg.append("g");
上記のg要素の中に、キーボードの各行を格納するg要素をネストします。
この時に、データとして先に作成した、3次元配列を渡すことで、g要素が5個DOMに追加されます。
これらのg要素には2次元配列が束縛され、実際のデータとしてdata[0]~data[4]がそれぞれ束縛されています。
// キーボードの各行を生成
var keyLines = keyboard.selectAll("g").data(data).enter().append("g");
ここでは、各キーそれぞれについてのg要素を作成します。
データとして親のg要素に束縛されているデータをそのまま渡します。
親のg要素には2次元配列が束縛されているので、生成されるg要素にはそれぞれ1次元配列が束縛されます。
つまり、ここでg要素に束縛されるデータは、["文字", "文字", コード]
となります。
// 各キー要素(この中にrectとtextを格納して1つのキーを表す)
var keys = keyLines.selectAll("g").data(function (d) { return d; }).enter().append("g");
そして、上で作成した各キーのg要素に対して見た目用のrectと印字用のtextを作成しています。
// キーを表す四角形と、印字されている文字をg要素の中に生成
var keyButtons = keys.append("rect");
var texts = keys.append("text");
エンターキーはrectでは表示できないので別途生成します。
// エンターキーは特別に別途生成する
var enterKey = keyboard.append("g");
var enterButton = enterKey.append("path");
var enterText = enterKey.append("text").text("Enter");
また、キーの影を表現するためにフィルターを作成します。
svgのフィルターについては、ここを参考ににしました。
// フィルターの作成
var filter = svg.append("filter").attr({
id: "drop-shadow",
width: "150%",
height: "150%",
})
filter.append("feGaussianBlur").attr({
in: "SourceAlpha",
result: "blur",
stdDeviation: 1,
})
filter.append("feOffset").attr({
dx: 0.5,
dy: 0.5,
result: "offsetBlur",
})
filter.append("feBlend").attr({
in: "SourceGraphic",
in2: "offsetBlur",
mode: "normal",
})
以上で、必要なすべての要素を生成できました。
###各要素の設定
ここからは、作成した要素についてそれぞれ設定をしていきます。
コードは結構長いですけど、行っていることは簡単な設定を多く行っているだけです。
svg.attr({
width: 800,
height: 300,
viewBox: "0 0 800 300",
})
// キーボードの枠設定
frame.attr({
rx: 5,
ry: 5,
x: 5,
y: 0,
width: 780,
height: 285,
fill: "#ddd",
filter: "url(#drop-shadow)",
})
// 各行を保持するg要素のyを設定していく
keyLines.attr({
transform: function (d, i) {
return "translate(20," + ((i * 50) + 20) + ")";
},
});
// キーの横幅からそのキーのxを計算していく
keys.attr({
transform: function (d, i, j) {
var ofset = 0;
for (var k = 0; k < i; k++) {
ofset += keyWdith[keyVariety[j][k]] + 10;
}
return "translate(" + ofset + ", 0)";
},
class: function (d, i) {
return "key" + d[2];
}
});
// キー自体の幅をなどを設定
keyButtons.attr({
rx: 5,
ry: 5,
width: function (d, i, j) {
return keyWdith[keyVariety[j][i]];
},
height: 40,
fill: "#f5f5f5",
stroke: "#aaa",
filter: "url(#drop-shadow)",
});
// 束縛されている文字や文字位置などから文字を設定
texts.attr({
x: function (d, i, j) {
if (textAlign[j][i] == "r") {
return keyWdith[keyVariety[j][i]] - 3; // 右よせの場合
}
return tx[textAlign[j][i]]; // 左よせの場合
},
y: function (d, i, j) {
return ty[textAlign[j][i]];
},
"text-anchor": function (d, i, j) {
return ta[textAlign[j][i]];
},
"font-size": function (d, i, j) {
if (textAlign[j][i] != "n") {
return 14;
}
return 20;
},
fill:"black"
}).text(function (d) {
return d[0] // 印字は自分に束縛されている配列の0盤目
});
// enterキーは別途設定
enterKey.attr({
"transform": "translate(694, 70)",
class: "key13",
});
enterButton.attr({
d: "M61,0Q66,0,66,5V85Q66,90,61,90H19Q14,90,14,85V45Q14,40,9,40H5Q0,40,0,35V5Q0,0,5,0Z",
fill: "#f5f5f5",
stroke: "#aaa",
filter: "url(#drop-shadow)",
})
enterText.attr({
x: 14,
y: 27,
"font-size": 16,
fill: "black",
})
このコードで重要な部分の解説をします。
この部分で、キーボードの各行に対してy軸方向の値を増加させています。
// 各行を保持するg要素のyを設定していく
keyLines.attr({
transform: function (d, i) {
return "translate(20," + ((i * 50) + 20) + ")";
},
});
そして、こちらで各キーのx軸方向の値を累積和で計算し、それぞれのキー位置を決定します。
また、位置の決定と同時に、すべてのキーに対してキー固有のクラスを付与しています。(フィードバックに使用)
// キーの横幅からそのキーのxを計算していく
keys.attr({
transform: function (d, i, j) {
var ofset = 0;
for (var k = 0; k < i; k++) {
ofset += keyWdith[keyVariety[j][k]] + 10;
}
return "translate(" + ofset + ", 0)";
},
class: function (d, i) {
return "key" + d[2];
}
});
最後に、エンターキーは特別に全て手打ちで設定します。
(この座標の設定がとても大変でした...)
// enterキーは別途設定
enterKey.attr({
"transform": "translate(694, 70)",
class: "key13",
});
enterButton.attr({
d: "M61,0Q66,0,66,5V85Q66,90,61,90H19Q14,90,14,85V45Q14,40,9,40H5Q0,40,0,35V5Q0,0,5,0Z",
fill: "#f5f5f5",
stroke: "#aaa",
filter: "url(#drop-shadow)",
})
enterText.attr({
x: 14,
y: 27,
"font-size": 16,
fill: "black",
})
ここでようやくキーボード部分は完成しました!
残りはキーの押下時に反応する部分です。
###フィードバックが起こるようにする
作ったキーボードが、ユーザーがキーを押下した時に反応するようにします。
キーの押下時と手が離れた時に呼ばれる関数を定義します。
function keydown(keyCode) {
var elem = (keyCode == 13) ? " path" : " rect";
if (keyCode == 16) { // shift押下時
shift(1);
}
var key = d3.selectAll(".key" + keyCode + elem).transition().duration(50).attr({
fill: "#d5d5d5",
filter: null,
})
var text = d3.selectAll(".key" + keyCode + " text").transition().duration(50).attr({
fill: "#E91E63",
})
}
function keyup(keyCode) {
var elem = (keyCode == 13) ? " path" : " rect";
if (keyCode == 16) { // shift押下時
this.shift(0);
}
var key = d3.selectAll(".key" + keyCode + elem).transition().duration(50).attr({
fill: "#eee",
filter: "url(#drop-shadow)",
})
var text = d3.selectAll(".key" + keyCode + " text").transition().duration(50).attr({
fill: "black",
})
}
それぞれの関数の中では、まずエンターキーかどうかを判別し、その情報を保持します。
その後シフトキーかどうかを判断し、シフトキーだった場合はshift関数(後述)を呼びます。
そして、キー情報をもとに対象をd3.selectAllにて見つけ、そのattributeを変更します。
また、shift押下時に呼ばれる関数は、以下のようにしました。
キー押下時は引数に1、離れたときは0を渡すことで、すべてのキーの印字を変更します。
function shift(num) {
keyTexts.text(function (d) {
return d[num];
})
}
そして、キー押下イベントの検出にはjQueryを使い、以下のようにしました。
$(window).keydown(function (e) {
keydown(e.keyCode);
})
$(window).keyup(function (e) {
keyup(e.keyCode);
})
以上で今回作ったキーボードが完成しました!
全てのコードを載せると長くなってしまうので、コード全体を見たい場合はこちらをご覧ください。
##おわりに
今回はインタラクティブなキーボードをSVGにて作成しました。
このキーボードは縦横の大きさを決め打ちにて設定しています。
ですが、このキーボードの大きさを変えるのは意外と簡単で、svg要素の設定にて、widthとheight書き換えるだけです。
これは、viewBoxを設定しているためです。詳しく知りたい方は、viewBoxについて検索してみてください。
実は、作成したタイピングゲームでは、このキーボードをアニメーションで表示させたり、非表示にしたりしました。(いつか解説するかもしれません)
こういった、拡張性に優れたd3が自分は大好きです。
svgとよく対比されている技術にcanvasがありますが、拡大しても汚くならないsvgのほうが自分は気に入っています。