Hatena::ブログ(Diary)

latest log このページをアンテナに追加 RSSフィード

2012-03-21 このエントリーを含むブックマーク このエントリーのブックマークコメント

(ε・◇・)っ ひっこした http://uupaa.hatenablog.com/

(ε・◇・)з そのうちここは綺麗サッパリ消えます

2011-10-21

New Features in Android Browser 4.0

via http://www.mobilexweb.com/blog/android-4-0-browser-html5

ブックマークの同期機能は iPhone Safari にも欲しい機能です。

手早くブックマークしておいて、あとからタブレットやPCで見るといった使い方ができますね。

Android Browser が SVG をサポート

これでブラウザ側のサポートはひと通り揃ったのですが。ツールや作業者のこなれ具合を鑑みると、

といった弱みがあります。これらをクリアしないと SVG ブームは来ないな〜 という感じしてます。

SVG DOM をサポートするライブラリが登場すれば、 SVGUI を構築する試みが始まり、そのへんから普及が始まるんじゃないかな〜 とも思いますが。

# uupaa.js 0.8 に仕込んでおいた SVG DOM ビルダー機能も、やっと陽の目を見るかな〜 と

2011-10-14

uupaa2011-10-14

mm.mfx 群舞向きアニメーションの実装

uupaa.js 0.8 で書いた uu.fx は1つのオブジェクトを高速で滑らかにアニメーションさせる能力に特化してまして、

沢山のオブジェクトが整列してパラパラと切り替わるタイプのアニメーションには使いづらいものでした。

ずっと宿題になってましたが、requestAnimationFrame を利用し、mm.mfxとして実装してみました。

# mfx は Mass Effectの略です

デモ: http://mofmof-js.googlecode.com/svn/trunk/test/Math.easing/tiling.htm

ブラウザ専用にコードを書いてないので、このままだとちょっと滑らかさが足りないのですが、ブラウザに特化した版ものちほど実装する予定です。

2011-09-25

(ε・◇・)з CoffeeScript と mofmof.js

(ε・◇・)з CoffeeScriptRubyPython っぽく書けて JavaScriptコンパイルできるプリティな言語だとか!

(ε・◇・)з 噂では CoffeeScript で書くとコードが短くなるとか!!

物は試しに http://jashkenas.github.com/coffee-script/ にある幾つかのコード片と、mofmof.js 混じりで書いた場合のコードを比べてみました。

(ε・◇・)з mofmof.js はここだよ → http://code.google.com/p/mofmof-js/w/list

Syntax
// coffee

    # Assignment:
    number   = 42
    opposite = true

    # Conditions:
    number = -42 if opposite

    # Functions:
    square = (x) -> x * x

    # Arrays:
    list = [1, 2, 3, 4, 5]

    # Objects:
    math =
      root:   Math.sqrt
      square: square
      cube:   (x) -> x * square x

    # Splats:
    race = (winner, runners...) ->
      print winner, runners

    # Existence:
    alert "I knew it!" if elvis?

    # Array comprehensions:
    cubes = (math.cube num for num in list)

// coffee -> js ----------------------------

    var cubes, list, math, num, number, opposite, race, square;
    var __slice = Array.prototype.slice;
    number = 42;
    opposite = true;
    if (opposite) number = -42;
    square = function(x) {
      return x * x;
    };
    list = [1, 2, 3, 4, 5];
    math = {
      root: Math.sqrt,
      square: square,
      cube: function(x) {
        return x * square(x);
      }
    };
    race = function() {
      var runners, winner;
      winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
      return print(winner, runners);
    };
    if (typeof elvis !== "undefined" && elvis !== null) alert("I knew it!");
    cubes = (function() {
      var _i, _len, _results;
      _results = [];
      for (_i = 0, _len = list.length; _i < _len; _i++) {
        num = list[_i];
        _results.push(math.cube(num));
      }
      return _results;
    })();

// mofmof ----------------------------

    // Assignment:
    var number = 42, opposite = true;

    // Conditions:
    opposite && (number = -42);

    // Functions:
    function square(x) { return x * x; }

    // Arrays:
    list = [1, 2, 3, 4, 5];

    // Objects:
    math = {
        root:   Math.sqrt,
        square: square,
        cube:   function(x) { return x * square(x); }
    };

    // Splats:
    function cube(winner, runners, /*... */) {
        return print.apply(null, arguments);
    }

    // Existence:
    elvis != null && alert("I knew it!");

    // Array comprehensions:
    cubes = list.filter(math.cube);

(ε・◇・)з 基本 Syntax だと、あまり変わった感じしませんね〜

Functions
// coffee

    square = (x) -> x * x
    cube   = (x) -> square(x) * x

    fill = (container, liquid = "coffee") ->
      "Filling the #{container} with #{liquid}..."

// coffee -> js ----------------------------

    var cube, square;
    square = function(x) {
      return x * x;
    };
    cube = function(x) {
      return square(x) * x;
    };
    var fill;
    fill = function(container, liquid) {
      if (liquid == null) liquid = "coffee";
      return "Filling the " + container + " with " + liquid + "...";
    };

// mofmof.js ----------------------------

    function square(x) { return x * x; };
    function cube(x)   { return square(x) * x; }

    function fill(container, liquid /* = coffee */) {
        return "Filling the @@ with @@...".f(container, liquid || "coffee");
    }

(ε・◇・)з Coffee は function が -> になってる分だけタイプ数少なめですね

Loops and Comprehensions
// coffee
    # Eat lunch.
    eat food for food in ['toast', 'cheese', 'wine']

// coffee -> js ----------------------------

    var food, _i, _len, _ref;
    _ref = ['toast', 'cheese', 'wine'];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      food = _ref[_i];
      eat(food);
    }

// mofmof ----------------------------

    ['toast', 'cheese', 'wine'].forEach(eat);

            or

    var iter = Hash(['toast', 'cheese', 'wine']), food;
    while (food = iter.next()) {
        eat(food);
    }

(ε・◇・)з mofmof.js はforEachやイテレータ使ってます。実行速度が重要でなければ、このへんは好みかな〜

// coffee

    countdown = (num for num in [10..1])

// coffee -> js ----------------------------

    var countdown, num;
    countdown = (function() {
      var _results;
      _results = [];
      for (num = 10; num >= 1; num--) {
        _results.push(num);
      }
      return _results;
    })();

// mofmof ----------------------------

    countdown = 10..$(1);

(ε・◇・)з mofmof.js は Number.prototype.$ を拡張して 10..$(1) で 10 から1までの配列を生成していますよ〜

// coffee

    yearsOld = max: 10, ida: 9, tim: 11

    ages = for child, age of yearsOld
      "#{child} is #{age}"

// coffee -> js ----------------------------

    var age, ages, child, yearsOld;
    yearsOld = {
      max: 10,
      ida: 9,
      tim: 11
    };
    ages = (function() {
      var _results;
      _results = [];
      for (child in yearsOld) {
        age = yearsOld[child];
        _results.push("" + child + " is " + age);
      }
      return _results;
    })();

// mofmof ----------------------------
    var yearsOld = { max: 10, ida: 9, tim: 11 };
    Hash.map(yearsOld, function(age, child) {
        return "@@ is @@".f(child, age);
    });

(ε・◇・)з Coffee側のコードはシンプルですね〜


Array Slicing and Splicing with Ranges
// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    copy    = numbers[0...numbers.length]

    middle  = copy[3..6]

// coffee -> js ----------------------------
    var copy, middle, numbers;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    copy = numbers.slice(0, numbers.length);
    middle = copy.slice(3, 7);

// mofmof ----------------------------
    var numbers = 0..$(9);

    var copy    = numbers.clip(0, numbers.length);

    var middle  = copy.clip(3, 6);

(ε・◇・)з もふもふ!

// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    numbers[3..6] = [-3, -4, -5, -6]

// coffee -> js ----------------------------
    var numbers, _ref;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    [].splice.apply(numbers, [3, 4].concat(_ref = [-3, -4, -5, -6])), _ref;

// mofmof ----------------------------
    numbers = 0..$(9);
    numbers.swap(3, (-3).$(-6));

(ε・◇・)з mofmof 側は、Array#swap配列の一部をごっそり入れ替えてます〜


Everything is an Expression (at least, as much as possible)
// coffee
    globals = (name for name of window)[0...10]

// coffee -> js ----------------------------
    var globals, name;
    globals = ((function() {
      var _results;
      _results = [];
      for (name in window) {
        _results.push(name);
      }
      return _results;
    })()).slice(0, 10); // 全部とってきて10個だけ返す

// mofmof ----------------------------
    globals = Hash.keys(window, 10); // 10個とってきて返す

(ε・◇・)з mofmof の Hash.keys は、第二引数で列挙する要素の最大数を指定できるので、10個だけ列挙して返します〜


Classes, Inheritance, and Super
// coffee

    class Animal
      constructor: (@name) ->

      move: (meters) ->
        alert @name + " moved #{meters}m."

    class Snake extends Animal
      move: ->
        alert "Slithering..."
        super 5

    class Horse extends Animal
      move: ->
        alert "Galloping..."
        super 45

    sam = new Snake "Sammy the Python"
    tom = new Horse "Tommy the Palomino"

    sam.move()
    tom.move()


// coffee -> js ----------------------------
    var Animal, Horse, Snake, sam, tom;
    var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
      for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
      function ctor() { this.constructor = child; }
      ctor.prototype = parent.prototype;
      child.prototype = new ctor;
      child.__super__ = parent.prototype;
      return child;
    };
    Animal = (function() {
      function Animal(name) {
        this.name = name;
      }
      Animal.prototype.move = function(meters) {
        return alert(this.name + (" moved " + meters + "m."));
      };
      return Animal;
    })();
    Snake = (function() {
      __extends(Snake, Animal);
      function Snake() {
        Snake.__super__.constructor.apply(this, arguments);
      }
      Snake.prototype.move = function() {
        alert("Slithering...");
        return Snake.__super__.move.call(this, 5);
      };
      return Snake;
    })();
    Horse = (function() {
      __extends(Horse, Animal);
      function Horse() {
        Horse.__super__.constructor.apply(this, arguments);
      }
      Horse.prototype.move = function() {
        alert("Galloping...");
        return Horse.__super__.move.call(this, 45);
      };
      return Horse;
    })();
    sam = new Snake("Sammy the Python");
    tom = new Horse("Tommy the Palomino");
    sam.move();
    tom.move();

// mofmof ----------------------------
    mm.Class("Animal", {
        init: function(name) {
            this.name = name;
        },
        move: function(meters) {
            alert("@@ moved @@m.".f(this.name, meters));
        }
    });
    mm.Class("Snake:Animal", {
        move: function() {
            alert("Slithering...");
            this.superCall("move", 5);
        }
    });
    mm.Class("Horse:Animal", {
        move: function() {
            alert("Galloping...");
            this.superCall("move", 45);
        }
    });
    var sam = mm("Snake", "Sammy the Python");
    var tom = mm("Horse", "Tommy the Palomino");

    sam.move();
    tom.move();

(ε・◇・)з mofmof にもクラスあるんだよ〜

(ε・◇・)з 最大継承数は1段に限定してある(昔は3段までサポートしてた)けど、1段継承できればニーズの9割カバーできるからOKなんだよ〜 (それ以上継承したい人は変態的なコードを書きたい人だけだと思うよ〜

Destructuring Assignment
// coffee
    theBait   = 1000
    theSwitch = 0

    [theBait, theSwitch] = [theSwitch, theBait]

// coffee -> js ----------------------------
    var theBait, theSwitch, _ref;
    theBait = 1000;
    theSwitch = 0;
    _ref = [theSwitch, theBait], theBait = _ref[0], theSwitch = _ref[1];

// mofmof ----------------------------
    var theBait = 1000, theSwitch = 0;

    [theBait, theSwitch].swap(0, [theSwitch, theBait]);

(ε・◇・)з ここでも Array#swap が出てきてます〜


Chained Comparisons
// coffee
    cholesterol = 127

    healthy = 200 > cholesterol > 60

// coffee -> js ----------------------------
    var cholesterol, healthy;
    cholesterol = 127;
    healthy = (200 > cholesterol && cholesterol > 60);

// mofmof ----------------------------
    var cholesterol = 127

    healthy = cholesterol.inRange(200, 60);

(ε・◇・)з Coffee のほうが分かりやすいね〜

おしまい

(ε・◇・)з mofmof.js も結構イケてるんじゃないでしょうか!

2011-09-14

Screen Transition Traversal Pattern

とある開発環境で iPhone/Android アプリを書いてます。開発言語は js ですが Titanium ではありません。

今回の開発で実現したい事の1つに「可能なら UI を自動でテストしたい」というのがあります。

ぼーっとしていたら、以下のようなロジック(パターン)を ピコーン しました (恐らくボクが知らないだけで車輪の再発明なんだろうけどさ!)

  • 画面をディレクトリと見立てる。ディレクトリ=画面。ファイルに該当するものは無し
  • 画面の状態をディレクトリパスにパラメタとして与えるだけで、ネストした画面を再現できる
    • /A(key1=value2;key2=value)/B/C といったパスは、画面Aを構築する際に key1=value2;key2=value といったパラメタが与えられる
    • テスト環境から cd /A(key1=value2;key2=value)/B/C と入力すると、画面Cが一番上に表示された状態が再現できる

一言でいうと、画面構造をREST風味にステートレス化するということです。途中の画面の状態を再現するパラメタをパスに埋め込む事ができ、再現する仕組みです。

このような構造を取ることにより、

  • 画面A が 画面B を知らず、画面B が 画面A を知らなくても成立する(かもしれない)
    • 開発終盤の仕様変更と再テストに強くなる。途中にどのような画面が新たに挟まっても手直しが少ない(かもしれない)
  • 画面A から 画面B に遷移する UI (ボタン等)には、最終的に /A/B といったパスが設定されるが、このパスは画面A が固定で持たず、UI 全体を統括するマネージャ的存在が 画面A に知らせる。

といった良いことがありそうです。

こんな感じ?

たたき台を考えてみました。mofmof.js ベースです。

閉じる/開く画面を決定するロジックのキモは、 mm.Class.Screen#resolve に実装する形になります(まだ空っぽです)

mm.Class.singleton("Screen", { // シングルトンクラスの定義。 mm("Screen").pwd() のように使用する
  _current: "/",               // カレントパス
  pwd: function() { // @return String: カレントパスを返す。pwd で通じるよね?
      return this._current;
  },
  cd: function(path) { // @param String: 移動先のパスを指定する
      var r = this.resolve(this._current, path); // 閉じる/開く画面の解決
      
      if (r.close.length) {
          // r.close の順に画面を破棄する
      }
      if (r.open.length) {
          // r.open の順に画面を破棄する
      }
      this._current = path; // カレントパスを更新
  },
  resolve: function(before,  // @param String:
                    after) { // @param String:
                             // @return Hash: { close, open, base }
                             //   close - StringArray:
                             //   open - StringArray:
                             //   base - String: base dir to open
    if (before === after) {
        return { close: [], open: [], base: "" };
    }

    var rv = { close: [], open: [], before: base: "" }, ...

    // closeする画面と、openする画面の配列 をここで作成する

    return rv;    
  }
});

予想では、resolve メソッドの結果は以下のようになるはずです。

mm("Screen").resolve("/A/B/C", "/A/B/C") -> { close: [], open: [], before: "/A/B/C", after: "/A/B/C" }
mm("Screen").resolve("/A/B/C", "/A/B") -> { close: ["C"], open: [], before: "/A/B/C", after: "/A/B" }
mm("Screen").resolve("/A/B/C", "/A/D") -> { close: ["C", "B"], open: ["D"], before: "/A/B/C", after: "/A/D" }
mm("Screen").resolve("/A/B/C", "/") -> { close: ["C", "B", "A"], open: [], before: "/A/B/C", after: "/" }
mm("Screen").resolve("/A/B/C", "/E/F") -> { close: ["C", "B", "A"], open: ["E", "F"], before: "/A/B/C", after: "/E/F" }

現在実装中なのですが、resolve が中々に手強いです。

実装できたらこのエントリを更新します。

追記

散々悩んで書いた汚いロジックがこちら。

「画面遷移どうすべ〜」→ ピコーン待ち → ピコーンきたー → ブログカキカキ → コードカキカキ 含めて8時間ぐらい。

    resolve: function(before,  // @param String:
                      after) { // @param String:
                               // @return Hash: { close, open, base }
                               //   close - StringArray:
                               //   open - StringArray:
                               //   base - String:
        if (before === after) { // match all -> nop
            return { close: [], open: [], base: "" };
        }

        var close = [], open = [], base = "/", bary, aary, last, i, iz;

        bary = before.split("/"); // "/A/B/C" -> ["", A, B, C]
        aary = after.split("/");  // "/A/B"   -> ["", A, B]

        // close
        while ((last = bary.pop())) {
            close.push(last);
            aary.length = bary.length;
            if (bary.join("/") === aary.join("/")) { // match
                break;
            }
        }
        bary = before.split("/"); // "/A/B/C" -> ["", A, B, C]
        aary = after.split("/");  // "/A/B"   -> ["", A, B]

        if (after === "/") { // case: resolve("/A/B/C", "/")
            ;
        } else if (bary[1] !== aary[1]) { // case: resolve("/A/B/C", "/E/F")
            // ルート直下から違う場合は全てopen対象となる
            base = "/";
            open = aary;
            open.shift();
        } else {
            // open ,, close とは逆方向に走査を行い不一致要素を open に追加する
            //      ,, 画面を開く際の起点となる要素をopenParentに設定する
            for (i = 1, iz = Math.max(bary.length, aary.length); i < iz; ++i) {
                if (aary[i] === void 0) { // outof index
                    break;
                }
                if (base) {
                    // 親画面が異なるため、親画面以下の全ての画面を追加する
                    if (bary[i] !== aary[i]) {
                        for (iz = aary.length; i < iz; ++i) {
                            open.push(aary[i]);
                            console.log("add @@".f(aary[i]));
                        }
                        break;
                    }
                } else {
                    base = bary[i];
                }
            }
        }
        return { close: close, open: open, base };
    }

(↑)のロジックは見て分かるように、最悪のコードです。日本語コメントが必要なくらいに最悪です。自分で書いてて「これはメンテできないわー」ってなりました。さらに幾つかのテストケースをパスできず「あかん、これはあかん…」とクソ悩んでました。

困ってしまって、2つ隣に座ってる同僚(セト神様)をティディベアに見立て、一人ティディベアデバッグ(夜中の4:00に一方的にブツブツ話かけたった)をした所、再度ピコーンが!!

セト神の啓示をうけ、5分でリライトしたロジックがこちら

    resolve: function(before,  // @param String:
                      after) { // @param String:
                               // @return Hash: { close, open, base }
                               //   close - StringArray:
                               //   open - StringArray:
                               //   base - String: base dir to open
        var close = [], open = [], ary1, ary2, base = "", i = 0, j = 0;

        if (before !== after) {
            ary1 = before.split("/"); // "/A/B/C" -> ["", A, B, C]
            ary2 = after.split("/");  // "/A/B"   -> ["", A, B]

            while (ary1[i] === ary2[i]) {
                base = ary1[i++] || "/";
            }
            j = i;
            while (ary1[i]) {
                close.push(ary1[i++]);
            }
            while (ary2[j]) {
                open.push(ary2[j++]);
            }
        }
        return { close: close.reverse(), open: open, base: base };
    }

テストコード

(function() {
'mm("Screen").resolve("/", "/")'.test('{ close: [], open: [], base: "" }');
'mm("Screen").resolve("/A", "/Z")'.test('{ close: ["A"], open: ["Z"], base: "/" }');
'mm("Screen").resolve("/", "/A/B/C")'.test('{ close: [], open: ["A","B","C"], base: "/" }');
'mm("Screen").resolve("/A", "/A/B/C")'.test('{ close: [], open: ["B","C"], base: "A" }');
'mm("Screen").resolve("/A/B/C", "/A/B/C")'.test('{ close: [], open: [], base: "" }');
'mm("Screen").resolve("/A/B/C", "/A/B")'.test(  '{ close: ["C"], open: [], base: "B" }');
'mm("Screen").resolve("/A/B/C", "/A/D")'.test(  '{ close: ["C", "B"], open: ["D"], base: "A" }');
'mm("Screen").resolve("/A/B/C", "/")'.test(     '{ close: ["C", "B", "A"], open: [], base: "/" }');
'mm("Screen").resolve("/A/B/C", "/E/F")'.test(  '{ close: ["C", "B", "A"], open: ["E", "F"], base: "/" }');
'mm("Screen").resolve("/A/B/C/D/E", "/A/Z/C/D/E")'.test('{ close: ["E", "D", "C", "B"], open: ["Z", "C", "D", "E"], base: "A" }');

"run".test("core.js - mm.Class.Screen");
})();

何が言いたいかというと、ティディベアデバッグ☆オススメ☆デス!!!!