よーし、パパ jQuery と CSS3 でフリック入力風の UI 作っちゃうぞ!の巻 その2

3連休をいかして、続きをやった結果を作業メモから転記。
前回はフリックUIの表示までやったんで、きちんと入力出来るところを目指してく。

現時点でフリック入力で花開く文字が、「あ」でも「か」でも「さ」でも「いうえお」しか出ない決め打ちなので、そこから

            left.css({
                'left' : (sx - 42) + 'px'
            }).text('い');
            right.css({
                'left' : (sx + 42) + 'px'
            }).text('え');
            top.css({
                'top' : (sy - 42) + 'px'
            }).text('う');
            bottom.css({
                'top' : (sy + 42) + 'px'
            }).text('お');

当然、ここに即値書いちゃうと重複コード祭りになっちゃうんで一般化する。
可読性(理解容易性)とトレードオフでキンキンにチューンする・・・(例えばコードゴルフするような)・・・場合には、違うかもやけど自分が書くときは基本的に、直観的なコードかつDRYかつ保守性(修正容易性)のバランスを考える。

ま、ここではバランスを考えるプロセスも書いてみよう、納期があるわけじゃないし実用性もあるわけや無いんで。
起点となるところは
あ → いうえお
か → きくけこ
さ → しすせそ
が取り出せれば良いので

        var map = {
            'a' : [ 'い', 'う', 'え', 'お' ],
            'ka' : [ 'き', 'く', 'け', 'こ' ],
            'sa' : [ 'し', 'す', 'せ', 'そ' ]
        };

こんな感じのデータ構造を考える。キーバリューって言うのか連想配列って言うのかマップっつーのかはどうでも良い。
HTML 要素に属性くわしといて

            var id = $(this).attr('id');
            left.css({
                'left' : (sx - 41) + 'px'
            }).text(map[id][0]);
            right.css({
                'left' : (sx + 41) + 'px'
            }).text(map[id][2]);
            top.css({
                'top' : (sy - 41) + 'px'
            }).text(map[id][1]);
            bottom.css({
                'top' : (sy + 41) + 'px'
            }).text(map[id][3]);

”0123”が”いうえお”と対応するのって直観的か?もちろん違う。
じゃあ、自然数で対応すると?
”1234”が”いうえお”でもやっぱ直観的じゃ無い。50音ってのを強調して”2345”が”いうえお”ならば?やっぱダメ。
番号と文字が対応するのはそないに直観的じゃ無いのな。
だけどプログラミングの世界だと、昔のメモリが非常に少なかった頃に作られたレガシーコードが生き残ってるのか、頭の中で変換するのが得意で直観的じゃ無いコードが平気な人が多いのか、何も考えずにコピペするのが横行してるだけなのか解んないけど、開発現場では直観的じゃ無いコードを良く見かける。

直観的じゃ無いコードはバグりやすいと思うんやけど、イマドキの現場ではテストコード書くのが常識なのでバグらないのかな?
数字と文字の対応が直観的じゃ無いとすると、文字(文字列)と文字の対応のが良いってことになる。


じゃあ

            left.css({
                'left' : (sx - 41) + 'px'
            }).text(map[id]['い']);
            right.css({
                'left' : (sx + 41) + 'px'
            }).text(map[id]['え']);
            top.css({
                'top' : (sy - 41) + 'px'
            }).text(map[id][’う’]);
            bottom.css({
                'top' : (sy + 41) + 'px'
            }).text(map[id]['お']);

↑これと↓これではどっちが良いか?

            left.css({
                'left' : (sx - 41) + 'px'
            }).text(map[id]['left']);
            right.css({
                'left' : (sx + 41) + 'px'
            }).text(map[id]['right']);
            top.css({
                'top' : (sy - 41) + 'px'
            }).text(map[id][top]);
            bottom.css({
                'top' : (sy + 41) + 'px'
            }).text(map[id]['bottom']);

個人的には上の奴のが直観的に思える。ただし、記号とか「わをん」の場合は違ってくる。
「慣れればこっちのが早い」と「直観的」はやや対立する概念かも知らん。「スタイリッシュ」と「ユーザビリティ」もやや対立するように。
じゃあ、直観的かどうかの観点では無く保守性から見ると、上の2つのコードは現実的には大差が無い。
その理由は
1.仕様の変更可能性が極めて低いこと(4近傍のフリック入力の変更の余地は低い)
2.単純な変更は文字・文字対応のマップで吸収するから
凝りすぎてもダメなのな。
「早すぎる最適化は諸悪の根源」 by クヌース
「最適化された組織は変化に弱い」 by デマルコ
逆に言えば、対応マップで変更を吸収するんやからそっちの保守性が重要ってことになる。生成を自動化するか書き換えやすくするかって判断基準はあるやろけど、ここではコーディングの話にしたいんで自動化の話はのけとく。
1つ目は思いついたまま書いたので

        var map = {
            'a' : [ 'い', 'う', 'え', 'お' ],
            'ka' : [ 'き', 'く', 'け', 'こ' ],
            'sa' : [ 'し', 'す', 'せ', 'そ' ]
        };

ちょっと面倒くさい。せめて

        var map = {
            'a' : 'いうえお',
            'ka' : 'きくけこ',
            'sa' : 'しすせそ'
        };

これぐらいじゃないとね。でも、これだと文字・文字マップにならないんで、きちんと文字・文字マップ版も書いてみる。

        var map = {
            'a' : {
                     'い' : 'い',
                     'う' : 'う',
                     'え' : 'え',
                     'お' : 'お'
                  },
            'ka' : {
                     'い' : 'か',
                     'う' : 'き',
                     'え' : 'く',
                     'お' : 'け'
                  }
        };

あーあーあーゼロ秒でダメコードなのが伝わるなあ。この DRY 違反は無いなあ。
ちなみに、こういうマップ作った場合の取り出しコードは

            left.css({
                'left' : (sx - 41) + 'px'
            }).text(map[id]['い']);
            right.css({
                'left' : (sx + 41) + 'px'
            }).text(map[id]['え']);
            top.css({
                'top' : (sy - 41) + 'px'
            }).text(map[id]['う']);
            bottom.css({
                'top' : (sy + 41) + 'px'
            }).text(map[id]['お']);

・・・これもそんなに直観的じゃ無いなあ。(笑)
てなことを試行錯誤しながら、結果的に

        var map = {
            'a' : 'いうえお',
            'ka' : 'きくけこ',
            'sa' : 'しすせそ'
        };
       
        //ボタンのクリック処理
        $('.button').mousedown(function() {
            var parent = $(this).parent();
            //jQuery UI の力も借りる
            var sx = $(this).position().left;
            var sy = $(this).position().top;
            var left = $(this).clone();
            left.css({
                'background-color' : 'whitesmoke',
                'z-index' : 1000
            }).addClass('temporal');
            var right = left.clone();
            var top = left.clone();
            var bottom = left.clone();
            var id = $(this).attr('id');
            left.css({
                'left' : (sx - 41) + 'px'
            }).text(map[id].charAt(0));
            right.css({
                'left' : (sx + 41) + 'px'
            }).text(map[id].charAt(2));
            top.css({
                'top' : (sy - 41) + 'px'
            }).text(map[id].charAt(1));
            bottom.css({
                'top' : (sy + 41) + 'px'
            }).text(map[id].charAt(3));
            parent.append(left);
            parent.append(right);
            parent.append(top);
            parent.append(bottom);
        });

こういうコードに。実際には、このままだと4近傍全部に文字割り振られてない場合どうすんねんとか問題はあるけど、そんなものは作りながら考える。
先に全部見通すのなんて無理やし、作ってる間に変えたくなる部分も出てくる筈。

ここまで作れたら、後は実際に入力する文字がどれになるかって言うマウスジェスチャーを判定するだけ。
必要なのは
1.クリックした文字の場合
2.上下左右にマウスを動かしてから、マウスを離した場合
iOSフリック入力で完全に45度の方角に移動した時にどういう文字入力になるのか解らないけど、どっちか決め打てば良いと思う。
もしかするとアップルぐらいの会社になれば、人間の指の可動域とかからデフォルトでの上優先か横優先かとか変えてる可能性やユーザー試験での感覚的なマッチングとかみてる可能性は高いかもしらん。

マウスダウンの座標とアップの座標は、座標系さえ揃ってれば単純にピタゴラスの定理で、座標系がそろってなくても回転も拡大縮小も無いなら、原点座標の調整で済む。
幸い jQuery のマウスイベントの pageX/pageY は座標系揃えてくれるんで、そのまま使う。
マウスアップ時の処理はこんな感じ。

        $(window).mouseup(function(e) {
            //ウィンドウ上の全てのマウスアップを拾うんで、入力が始まってない時は無視する
            if (pos.left == 0 && pos.top == 0) return;
            var width = e.pageX - pos.left;
            var height = e.pageY - pos.top;
            var dist = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
            var msg = $('#msg2').val();
            //スレッショルドは約15ピクセル
            if (dist <= 15) {
                msg = msg + map[id].charAt(0);
            } else {
                if (Math.abs(width) > Math.abs(height)) {
                    //横向き
                    if (width < 0) {
                        msg = msg + map[id].charAt(1);
                    } else {
                        msg = msg + map[id].charAt(3);
                    }
                } else {
                    //縦向き
                    if (height < 0) {
                        msg = msg + map[id].charAt(2);
                    } else {
                        msg = msg + map[id].charAt(4);
                    }
                }
            }
            $('#msg2').val(msg);
            //後始末
            $('.temporal').remove();
            pos.left = 0;
            pos.top = 0;
        });


大体こんな感じ。