latest log

酩酊状態で書いたエンジニアポエムです。酩酊状態で読んでください。

uuCanvas.js のサブセットとして VMLCanvas.js を切り出しました + 速くしました

uuCanvas.js は、ExplorerCanvas をヒントに作成した、canvas を VML, Silverlight, Flash でレンダリングするライブラリです。

uuCanvas.js から VML 限定版として VMLCanvas.js を切り出しました。
# uuCanvas.js は 1万行。VMLCanvas.js は 2000行 です。


VMLCanvas.js は、機能的に ExplorerCanvas の上位互換となっており、より高機能、より高速、より簡単です(たぶん)。

uuCanvas.js と違い、VMLCanvas.js は外部ライブラリに依存しません。mofmof.js にも依存しておらず単体で機能します。

IE 8 で canvas が使いたくなった場合に、候補の一つとして検討してみてください。

説明 | デモ | ダウンロード | ソースコード

追記

VMLCanvas.js-1.1.0.js 公開しました。

以下の変更とメソッドを追加しています。

  • mod CanvasRenderingContext2D#lock
  • add CanvasRenderingContext2D#animFillPath
  • add CanvasRenderingContext2D#animStrokePath
  • add CanvasRenderingContext2D#animFillRect
  • add CanvasRenderingContext2D#animStrokeRect

上記のメソッドが適用できる Jump Dots のケースでは、ExplorerCanvas に比べて、5~10%程度CPU負荷が低減し、20~30%程度高速化しています。

速度比較動画はこちらから http://screencast.com/t/C5jbJWSOVZSP ExplorerCanvasuuCanvas.js (VML Backend) → VMLCanvas.js

アニメーション系 API の仕組み

anim*** API は、一度生成した VMLNode のリサイクルを行うことで、insertAdjacentHTML によるノードの生成と innerHTML = "" による削除時間をカットしています。
2回目以降の呼び出しでは、生成済みの VMLNode を検索し、パス情報と色情報だけを更新することで描画を更新しています。

また、lock() の実装を変更し、
lock(false) で canvas を display:none に変更し、unlock() で display:inline-block に戻すことで、リサイクル中のちらつきを極力低減しています。

内部実装の都合がAPIにまで出てしまっており、かなり残念な感じもありますが、
喉から手が出るほど速度が欲しい場合に、こういう奥の手があるよということで。

// --- Extends ---------------------------------------------
// CanvasRenderingContext2D.prototype.lock
function lock(clear) { // @arg Boolean(= false): lazy clear
    if (this._lockState & 0x1) {
        throw new TypeError("DUPLICATE_LOCK");
    }
    this._lockState = 0x1;
    if (clear) {
        this._lockState |= 0x2;
    } else {
        this._lockState |= 0x4;
        this._view.style.display = "none";
    }
}

// CanvasRenderingContext2D.prototype.unlock
function unlock() {
    if (this._lockState & 0x1) {
        if (this._lockState & 0x2) {
            this.clear();
        }
        if (this._lockStack.length) {
            this._view.insertAdjacentHTML("BeforeEnd", this._lockStack.join(""));
            this._lockStack = [];
        }
        if (this._lockState & 0x4) {
            this._view.style.display = "inline-block";
        }
        this._lockState = 0x0;
    }
}

// CanvasRenderingContext2D.prototype.clear
function clear() {
    // reset state
    this._zindex = 0;
    this._view.innerHTML = ""; // clear all
}

function animFillPath(id,      // @arg String: id
                      x,       // @arg Number: moveTo(x)
                      y,       // @arg Number: moveTo(y)
                      lines) { // @arg NumberArray: lineTo([<x0, y0>, <x1, y1>, ...])
    this.animStrokePath(id, x, y, lines, true);
}

function animStrokePath(id,     // @arg String: id
                        x,      // @arg Number: moveTo(x)
                        y,      // @arg Number: moveTo(y)
                        lines,  // @arg NumberArray: lineTo([<x0, y0>, <x1, y1>, ...])
                        fill) { // @arg Boolean(= false): true is fill
    var path = [];

    // --- create path ---
    {
        // moveTo(x, y)
        var m = this._matrix,
            ix = (x * m[0] + y * m[3] + m[6]) * 10 - 5,
            iy = (x * m[1] + y * m[4] + m[7]) * 10 - 5;

        path.push("m ", Math.round(ix), " ", Math.round(iy));

        var i = 0, iz = lines.length;
        // lineTo(x, y)
        for (; i < iz; i += 2) {
            x  = lines[i];
            y  = lines[i + 1];
            ix = (x * m[0] + y * m[3] + m[6]) * 10 - 5;
            iy = (x * m[1] + y * m[4] + m[7]) * 10 - 5;
            path.push("l ", Math.round(ix), " ", Math.round(iy));
        }
        path.push(" x");
    }

    var node = _nodeCache[id];

    if (node) {
        node.path = path.join("");

        var color = _color(fill ? this.fillStyle : this.strokeStyle),
            child = node.firstChild;

        child.color = color.hex;
        child.opacity = this.globalAlpha * color.a;
    } else {
        _applyProperties(this, fill);
        var fg;

        if (fill) {
            fg = '<v:shape id="' + id + '" style="position:absolute;width:10px;height:10px;z-index:0' +
                    '" filled="t" stroked="f" coordsize="100,100" path="' + path.join("") +
                    '"><v:fill color="' + this.__fillStyle.hex +
                    '" opacity="' + (this.globalAlpha * this.__fillStyle.a) + '" /></v:shape>';
        } else {
            var props = _buildStrokeProps(this);

            fg = '<v:shape id="' + id + '" style="position:absolute;width:10px;height:10px;z-index:0' +
                    '" filled="f" stroked="t" coordsize="100,100" path="' + path.join("") +
                    '"><v:stroke color="' + this.__strokeStyle.hex +
                    '" opacity="' + (this.globalAlpha * this.__strokeStyle.a) + props + '" /></v:shape>';
        }

        this._view.insertAdjacentHTML("BeforeEnd", fg);
        _nodeCache[id] = document.getElementById(id); // { id: node }
    }
}

// CanvasRenderingContext2D.prototype.animFillRect
function animFillRect(id, x, y, w, h) {
    this.animStrokeRect(id, x, y, w, h, 1);
}

// CanvasRenderingContext2D.prototype.animStrokeRect
function animStrokeRect(id,     // @arg String: id
                        x,      // @arg Number:
                        y,      // @arg Number:
                        w,      // @arg Number:
                        h,      // @arg Number:
                        fill) { // @arg Boolean(= false): true is fill

    var node = _nodeCache[id];

    if (node) {
        node.path = _rect(this, x, y, w, h);

        var color = _color(fill ? this.fillStyle : this.strokeStyle),
            child = node.firstChild;

        child.color = color.hex;
        child.opacity = this.globalAlpha * color.a;
    } else {
        this.stroke(_rect(this, x, y, w, h), 1);
        _nodeCache[id] = document.getElementById(id); // { id: node }
    }
}