l'essentiel est invisible pour les yeux

Showing posts with label Javascript. Show all posts
Showing posts with label Javascript. Show all posts

Thursday, October 09, 2008

5倍速いSquirrelFish Extremeの正規表現エンジンWREC

「recursive callを得意とするV8が、recursive callに比重をおいた、V8 Benchmark Suiteで速い!速い!と語られるのはフェアではない」

今回、正規表現のみに焦点を当ててJavaScriptエンジンを比較してみた。

環境

  • SpiderMonkey - 1.7.0 2007-10-03
  • SFX - Revision: 37445
  • v8 - Revision: 389

SFX(SquirrerlFish Extreme)に新しく搭載されたWREC("the WebKit Regular Expression Compiler")、Safari 3に搭載されているJavaScript Coreに比べて、なんと5倍も速い。正規表現の実行が占める実行時間が、Webアプリケーション全体の実行時間の3%ほどだとすれば、WRECは、2.4%ほどの高速化に貢献している。

V8, SFX, SpiderMonkey, Safari 3でregexp-dna.htmlをベースに作成したベンチマークスクリプトでベンチマークを計測した。スループット(1分間の実行回数)を算出しているので、グラフが大きい方が高性能。(5回測定した結果の中央値を利用)



SFXが、Safari 3の6.44倍も速いという結果がでた!!公式に発表されている結果では、5倍だからそれよりも速い!が、String.prototype.replaceメソッドの実装に差があるため、この結果はフェアではない。regex-dna.jsを修正し、実行した結果は次の通り。

実行結果


公式の発表に近い、Safari 3の5.19倍速いという結果がでたので、概ね正しそうだ。

regexp-dna.jsの修正内容は、次のとおり。

< dnaInput = dnaInput.replace(k, subs[k], "g")
---
> dnaInput = dnaInput.replace(new RegExp(k, 'g'), subs[k]);


String.prototype.replaceの実装の差
ECMAScript262によるString.prototype.replace(searchValue, replaceValue)の定義では、第三引数は受付けないが、MozillaのSpiderMonkeyでは、第一引数に文字列が渡された場合に、第三引数に正規表現フラグを指定できるように拡張している。

もうひとつ。SpiderMonkey(js), SFX(jsc), v8(v8-shell)でString.prototype.replace関数の比較をすると、SFXの実装だけ明らかに他の2つとは異なることがわかる。


% js
js> 'aaaaa'.replace('a', '_$&_', 'g') # SpiderMonkeyでは、gフラグが有効
_a__a__a__a__a_

% v8-shell
V8 version 0.2.5
> 'aaaaa'.replace('a', '_$&_', 'g') # v8では、gフラグは無効
_a_aaaa

% jsc
> 'aaaaa'.replace('a', '_$&_', 'g') # SFXでも、gフラグは無効
_$&_aaaa
>'aaaaa'.replace(/a/, '_$&_', 'g')
_a_aaaa


WRECでは、第一引数が通常の文字列の場合は、通常の文字列置換が行われ、第二引数の置換テキスト($&)が展開されない。SFX中では、"WebKit/JavaScriptCore/kjs/StringPrototype.cpp"にString.prototype.replaceの実装があるが、文字列を探索して、マッチした以前の部分、置換する文字列、マッチした後の文字列を連結して返している。

WebKit/JavaScriptCore/kjs/StringPrototype.cpp

JSValue* stringProtoFuncReplace(ExecState* exec, JSObject*, JSValue* thisValue, const ArgList& args)
{
// 引数の処理
if (pattern->isObject(&RegExpObject::info)) {
// 第一引数がRegExpオブジェクトのケースの処理
}
// 第一引数が文字列の場合
// First arg is a string
UString patternString = pattern->toString(exec);
int matchPos = source.find(patternString);
int matchLen = patternString.size();
// Do the replacement
if (matchPos == -1)
return sourceVal;

if (callType != CallTypeNone) {
ArgList args;
args.append(jsSubstring(exec, source, matchPos, matchLen));
args.append(jsNumber(exec, matchPos));
args.append(sourceVal);

replacementString = call(exec, replacement, callType, callData, exec->globalThisValue(), args)->toString(exec);
}

return jsString(exec, source.substr(0, matchPos) + replacementString + source.substr(matchPos + matchLen));
}


String.prototype.replaceの第一引数が文字列の場合に、単純な文字列置換をおこないパフォーマンスを向上する最適化は、SpiderMonkeyでもとりこまれる予定だ。(See Bug 432525)

WRECでは、グルーピングは未実装
シンプル!軽量!速い!と3拍子そろったWRECだけど、グルーピングの機能は実装しておらず、グルーピング付きのパターンが指定された時は、PCREに処理を任せることになるため、実行速度が低下する。


bool WRECParser::parseParentheses(JmpSrcVector&)
{
// FIXME: We don't currently backtrack correctly within parentheses in cases such as
// "c".match(/(.*)c/) so we fall back to PCRE for any regexp containing parentheses.

m_err = TempError_unsupportedParentheses;
return false;
}


regepx-dna.htmlのベンチマークでグルーピングありとなしを比べると実行速度の差が明らかになる。実行時間が変更前の140%になったSpiderMonkeyに比べて、SFXは、680%もの増加となった。

変更前

> dnaInput = dnaInput.replace(/g*t/g, subs[k]);



% jsc regexp-dna.js
285 (msec)

% js regepx-dna.js
1789 (msec)


変更後 (SFXでは、680%の速度低下)

> dnaInput = dnaInput.replace(/(g*)t/g, subs[k]);


% jsc regexp-dna.js
1942 (msec)

% js regexp-dna.js
2501 (msec)

Saturday, January 19, 2008

[Javascript] "うごかせるモノである”事をアフォードするiPhone firmware1.1.3のEffect.wobbleを実装した。(Effect.illuminateと組み合わせたデモ)

iPhone / iPod touchユーザの皆さんは、Firmware1.1.3にバージョンアップしましたか?
Jail Breakが面倒なので、私のiPhoneは1.1.1のままですが、1.1.3にアップグレードしたiPod touchを入手したので、作りました。

「あるモノが動かせる」事をユーザに一番早く理解してもらうにはどうすれば良いか?
おそらく、ブラウザ上で一番使われているのは、CSSでマウスカーソルをcursor:moveにする方法だろう。それなりにコンピュータに触れているユーザならば、このアイコンが出れば「ん?動かせるのかな?」と気になる。しかし、マウスポインタの概念が存在しないiPhone / iPod touchではこの方法は使えない。iPhone / touch では、より動物の持つアフォーダンスに働きかける方法を採用している。


Apple Patent shows details of iPhone 1.1.3 firmware

「(突然)動く、点滅する」これらのアクションは、人間の注意を引きつける。私達の住む世界でも、信号、ハザードのような注意を引きつける必要があるものは、点灯->点滅へと遷移する事が多い。では、「あるものが動かせる物である」ということをアフォードするにはどうするのがいいだろうか?
ネコでもイヌでも、今まで静止していたものが突然、ぐらぐらと揺れ始めたら、警戒するだろう。人間も同じで、それまで静止していた物がバランスを崩し、ぐらぐらとしだしたら、その不安定な状態 == 何か動かせるものと直感的に感じ取ることができる。

Effect.wobble and Effect.illuminate demo


あるオブジェクトがぐらぐらと揺れるエフェクト(Effect.wobble)をJavascriptで実装した。以前作成した、Effect.illuminateと組み合わせたデモを作った。(excanvasを使えば、IEでも動作するかもしれませんが、IEが無いのでテストできていません。Firefox 2.x or Safariで動作確認しています。)


「早速、CPUが高速回転し始めましたか?」
仕組みは、それほど複雑ではないが少しトリッキーな事をしている。
  1. このエフェクトは、画像にしか適用できない。
  2. Effect.wobberが最初に呼び出された時点で、canvasを作成する。(一度作成されたcanvasはキャッシュ)
  3. canvasのサイズは、画像の斜辺の長さの正方形に設定し、canvasの中心を画像の中心と合わせる。(キャンバスが画像と同じサイズでは、回転させた時に表示されない部分が出るため。)
  4. canvasに画像をレンダリングし、元の画像はdisplay:noneにするのではなく、別の画像(spacer.gif)を読み込ませて強制的に、元画像と同じサイズに設定する。この方法でないと、画像を隠した段階でブラウザにより強制的に再レイアウトされるため、レイアウトが崩れる。
  5. canvas上の画像は、指定した角度(デフォルトでは1.5度)に、時計回り、反時計回りを交互に繰り返す。


How to wobbling images
使い方は簡単で、三つのオプションを渡せる。degreeは回転させる角度を指定する(デフォルトは、1.5°)。durationは何秒間ぐらぐらさせるか。デフォルトでは、永続的にエフェクトが実行される。秒数を指定する事で経過秒後にエフェクトが停止する。freequencyは、ぐらぐらさせる関数を呼び出すインターバル値を指定するパラーメタでミリ秒(デフォルト 90 msec)で指定する。(秒に統一した方がよい)


$$('img.wobble').each(function(img) {
img.observe('mouseover', function(event) { event.target.wobble({duration: 2, degree: 1.5}) });
});


Effect.illuminate


このエフェクトは、iPhone / iPod touchのロック解除前に表示されるエフェクト。文字列の上をスポットライトが左から右に照らすように動く。仕組みは次のようになっている。
  1. 全ての文字をspanタグで置換
  2. 順番にスポットライトの色(オプションで指定可能)でstyleを変更していく。
  3. 最後まで照らし終わったらまた最初の文字からスタートする。


Source code
興味があればどぞ。

// The MIT License
// Copyright (c) 2008 Rakuto Furutani, All Rights Reserved.
// mail: rakuto at gmail.com
// blog: http://rakuto.blogspot.com/

// Add arbitrary methods as HTML#{tag}Element instance methods
// See: http://rakuto.blogspot.com/2008/01/javascripts-element.html
Element.addMethodsByTag = function(aTag, aMethods) {
if(aMethods.constructor == Object) {
var methods = new Object();
methods[aTag.toUpperCase()] = aMethods;
Object.extend(Element.Methods.ByTag, methods);
Element.addMethods();
}
};

// Effect object
var Effect = Effect || {};
Object.extend(Effect, (function() {
// NOTE: You need to replace it.
var SPACER_PATH = '/images/spacer.gif';

// The canvas will be used for wobbling effect
function createCanvas(element)
{
var attrs = {width: element.width, height: element.height};
var style = {zIndex: 0, display: 'none', position: 'absolute'};
var ctx, canvas = Element.extend(document.createElement('canvas'));
canvas.writeAttribute(attrs).setStyle(style);
(element.parentNode || document.body).appendChild(canvas);
if(ctx = canvas.getContext('2d')) {
ctx.drawImage(element, 0, 0);
return canvas;
} else {
throw new Exception('Effect.wobble requires fecture of HTMLCanvasElement.');
}
}
function saveOriginal(element)
{
var width = element.width, height = element.height;
element._originalSrc = element.src;
element.src = SPACER_PATH;
element.width = width;
element.height = height;
element.setStyle({width: width, height: height});
return element;
}
function restoreOriginal(element)
{
element.src = element._originalSrc;
return element;
}
// Called when stop the wobbling effect
function teardown(element)
{
element._wobbling = false;
restoreOriginal(element).show()._canvas.hide();
}

return {
wobble: function(element, options) {
if(element._wobbling) return;
var image, radian, ctx, clockwise = -1;
var width = element.width, height = element.height, offset = element.cumulativeOffset();
var sidelen = Math.ceil(Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)));
var dx = Math.ceil((sidelen - width) / 2), dy = Math.ceil((sidelen - height) / 2);
// Default options
options = Object.extend({
degree: 1.5,
frequency: 90,
duration: 0
}, options);
element._canvas = element._canvas || createCanvas(element);
element._canvas.writeAttribute({width: sidelen, height: sidelen}).setStyle({
left: offset[0] - dx + 'px', top: offset[1] - dy + 'px'}).show();
ctx = element._canvas.show().getContext('2d');
ctx.translate(dx, dy);
saveOriginal(element)._canvas.show();
radian = Math.PI / 180 * options.degree;

// Start the effect animation
image = new Image();
image.src = element._originalSrc;
element._wobbling = setInterval(function() {
ctx.clearRect(-dx, -dy, sidelen, sidelen);
ctx.save();
ctx.translate(Math.ceil(width / 2), Math.ceil(height / 2));
ctx.rotate((clockwise *= -1) * radian);
ctx.drawImage(image, -(width / 2), -(height / 2));
ctx.restore();
}, options.frequency);

// If option.duration isn't zero, then the effect will be stopped
// after a lapse of option.duration secounds.
if(options.duration != 0) teardown.delay(options.duration, element);

return element;
},
// Stop the wobbling effect
stopWobble: function(element) {
if(element._wobbling) teardown(element);
return element;
},
illuminate: function(element, options) {
var effect = arguments.callee;
if(element.tagName) {
// Save the original style
with(effect) {
color = element.getStyle('color');
innerHTML = element.innerHTML;
}
$A(element.childNodes).each(function(node) {effect(node, options)});
return;
}

// It's only executed when node is text node.
options = Object.extend({
color: '#ffffff',
size: 4,
repeat: true,
interval: 70
}, options);

// The all characters is replaced because accessing the innerHTML property is pretty slow.
var parent = element.parentNode;
parent.innerHTML = $A(element.nodeValue).map(function(text) {
return ['<span style="color: ', this.color, '">', text, '</span>'].join('');
}).join('');

var self = this, started = false;
var turnOn = function(node) {node.style.color = options.color};
var turnOff = function(node) {node.style.color = self.color};
var restore = function() {parent.innerHTML = self.innerHTML};
var startTurnOffEffect = function(callback) {
started = true;
$A(parent.childNodes).inject(0, function(delay, node, idx) {
with({idx: idx}) {
setTimeout(function() {
turnOff(node);
if(callback['onEnd'] && idx == parent.childNodes.length - 1) callback['onEnd']();
}, delay);
}
return delay + options.interval;
});
};
// Start the effect
$A(parent.childNodes).inject(0, function(delay, node, idx) {
setTimeout(function() {
turnOn(node);
if(!started && idx > options.size) {
startTurnOffEffect({onEnd: function() {
restore();
if(options.repeat) effect(parent, options);
}});
}
}, delay);
return delay + options.interval;
});
}
};
})());

// Add some method as Element's instance methods
Element.addMethodsByTag('img', {
wobble: Effect.wobble,
stopWobble: Effect.stopWobble
});
Element.addMethods({
illuminate: Effect.illuminate
});

Event.observe(window, 'load', function(event) {
// Start the wobbling effect
$('img.wobble').each(function(img) {
img.wobble();
});

$('img.wobbleOnMouseOver').each(function(img) {
img.observe('mouseover', function(event) { event.target.wobble({duration: 2}) });
});

// Start the Illuminate Effect
$('nuts').illuminate();
});



P.S.
ドバイの夜景が綺麗だ。ドバイに行きたい。最後の夜景は大阪の梅田。
久しく夜景見てないなぁ。モテモテモテるコードが書けるようになりたい。

[Javascript] iPhone's two pretty cool effects - Effect.wobble and Effect.illuminate

This entry for Japanese is here.

I've impemented two interesting effects in iPhone, Effect.wobble and Effect.illuminate. You can see the wobbering effect in iPhone / iPod touch's firmware 1.1.3. The wobbering effect is able to inform users about draggable object. A following movie is demo of the wobbering effect.



You can see the Effect.illuminate on the top page in iPhone and iPod touch. You've seen "slide to unlock"'s effect, haven't you? The Effect.illuminate is just it.

Demo of the Effect.wobble and Effect.illuminate


These effects are resource-hungry. This is demo, and Firefox 2.x and Safari only. Maybe Effect.wobble will be working correctly on IE with ExplolerCanvas, but I haven't tested yet.


How to use it

// Effect.wobble
$('img.wobble').each(function(img) {
img.observe('mouseover', function(event) { event.target.wobble({duration: 2, degree: 1.5}) });
});

// Effect.illuminate
$('illuminated_msg').illuminate({color: '#fff'});


How to work - Effect.wobble
  1. The mecanism is simple, but there are some tricky technics.
  2. The Effect.wobble can only call for IMG element.
  3. Create a canvas element for wobbering effect when the effect will be called at first.
  4. The width and height of canvas are same that an image's length of oblique line.
  5. Fit in position of canvas element in order to align image's center position.
  6. Draw an image on canvas, and the original image never set 'style="display: none"'. If display property will be none, then the layout will be broken. So we set src property with dummy image. (spacer.gif)
  7. Run the effect to rotate image drawing on canvas. The effect will change the clockwise rotation and anticlock rotation alternately.


How to work - Effect.illuminate
  • At first, all characters is replaced with span element.
  • Start the turn on effect and turn off effect ATST.


Other demo of Effect.illuminate




Source code

// The MIT License
// Copyright (c) 2008 Rakuto Furutani, All Rights Reserved.
// mail: rakuto at gmail.com
// blog: http://rakuto.blogspot.com/

// Add arbitrary methods as HTML#{tag}Element instance methods
// See: http://rakuto.blogspot.com/2008/01/javascripts-element.html
Element.addMethodsByTag = function(aTag, aMethods) {
if(aMethods.constructor == Object) {
var methods = new Object();
methods[aTag.toUpperCase()] = aMethods;
Object.extend(Element.Methods.ByTag, methods);
Element.addMethods();
}
};

// Effect object
var Effect = Effect || {};
Object.extend(Effect, (function() {
// NOTE: You need to replace it.
var SPACER_PATH = '/images/spacer.gif';

// The canvas will be used for wobbling effect
function createCanvas(element)
{
var attrs = {width: element.width, height: element.height};
var style = {zIndex: 0, display: 'none', position: 'absolute'};
var ctx, canvas = Element.extend(document.createElement('canvas'));
canvas.writeAttribute(attrs).setStyle(style);
(element.parentNode || document.body).appendChild(canvas);
if(ctx = canvas.getContext('2d')) {
ctx.drawImage(element, 0, 0);
return canvas;
} else {
throw new Exception('Effect.wobble requires fecture of HTMLCanvasElement.');
}
}
function saveOriginal(element)
{
var width = element.width, height = element.height;
element._originalSrc = element.src;
element.src = SPACER_PATH;
element.width = width;
element.height = height;
element.setStyle({width: width, height: height});
return element;
}
function restoreOriginal(element)
{
element.src = element._originalSrc;
return element;
}
// Called when stop the wobbling effect
function teardown(element)
{
element._wobbling = false;
restoreOriginal(element).show()._canvas.hide();
}

return {
wobble: function(element, options) {
if(element._wobbling) return;
var image, radian, ctx, clockwise = -1;
var width = element.width, height = element.height, offset = element.cumulativeOffset();
var sidelen = Math.ceil(Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)));
var dx = Math.ceil((sidelen - width) / 2), dy = Math.ceil((sidelen - height) / 2);
// Default options
options = Object.extend({
degree: 1.5,
frequency: 90,
duration: 0
}, options);
element._canvas = element._canvas || createCanvas(element);
element._canvas.writeAttribute({width: sidelen, height: sidelen}).setStyle({
left: offset[0] - dx + 'px', top: offset[1] - dy + 'px'}).show();
ctx = element._canvas.show().getContext('2d');
ctx.translate(dx, dy);
saveOriginal(element)._canvas.show();
radian = Math.PI / 180 * options.degree;

// Start the effect animation
image = new Image();
image.src = element._originalSrc;
element._wobbling = setInterval(function() {
ctx.clearRect(-dx, -dy, sidelen, sidelen);
ctx.save();
ctx.translate(Math.ceil(width / 2), Math.ceil(height / 2));
ctx.rotate((clockwise *= -1) * radian);
ctx.drawImage(image, -(width / 2), -(height / 2));
ctx.restore();
}, options.frequency);

// If option.duration isn't zero, then the effect will be stopped
// after a lapse of option.duration secounds.
if(options.duration != 0) teardown.delay(options.duration, element);

return element;
},
// Stop the wobbling effect
stopWobble: function(element) {
if(element._wobbling) teardown(element);
return element;
},
illuminate: function(element, options) {
var effect = arguments.callee;
if(element.tagName) {
// Save the original style
with(effect) {
color = element.getStyle('color');
innerHTML = element.innerHTML;
}
$A(element.childNodes).each(function(node) {effect(node, options)});
return;
}

// It's only executed when node is text node.
options = Object.extend({
color: '#ffffff',
size: 4,
repeat: true,
interval: 70
}, options);

// The all characters is replaced because accessing the innerHTML property is pretty slow.
var parent = element.parentNode;
parent.innerHTML = $A(element.nodeValue).map(function(text) {
return ['<span style="color: ', this.color, '">', text, '</span>'].join('');
}).join('');

var self = this, started = false;
var turnOn = function(node) {node.style.color = options.color};
var turnOff = function(node) {node.style.color = self.color};
var restore = function() {parent.innerHTML = self.innerHTML};
var startTurnOffEffect = function(callback) {
started = true;
$A(parent.childNodes).inject(0, function(delay, node, idx) {
with({idx: idx}) {
setTimeout(function() {
turnOff(node);
if(callback['onEnd'] && idx == parent.childNodes.length - 1) callback['onEnd']();
}, delay);
}
return delay + options.interval;
});
};
// Start the effect
$A(parent.childNodes).inject(0, function(delay, node, idx) {
setTimeout(function() {
turnOn(node);
if(!started && idx > options.size) {
startTurnOffEffect({onEnd: function() {
restore();
if(options.repeat) effect(parent, options);
}});
}
}, delay);
return delay + options.interval;
});
}
};
})());

// Add some method as Element's instance methods
Element.addMethodsByTag('img', {
wobble: Effect.wobble,
stopWobble: Effect.stopWobble
});
Element.addMethods({
illuminate: Effect.illuminate
});

Event.observe(window, 'load', function(event) {
// Start the wobbling effect
$('img.wobble').each(function(img) {
img.wobble();
});

$('img.wobbleOnMouseOver').each(function(img) {
img.observe('mouseover', function(event) { event.target.wobble({duration: 2}) });
});

// Start the Illuminate Effect
$('nuts').illuminate();
});

Monday, January 14, 2008

[Javascript] Prototype.js 1.6.0 - Event.fireでNative DOM Eventを呼び出し可能にする

Javascriptの話題が続いてます。

Changeset 7835 - prototype: Namespace all custom event names to avoid conflicts with native DOM events.

上記Changesetで、Native DOM Eventとの名前衝突を避けるために、コロンで区切るネームスペースが導入された & Native DOM Eventの呼び出しが削除された?ため、Native DOM Eventが呼び出せない。[Javascript] クロージャを利用したイベントリスナの登録のように、クロージャー + 無名関数でイベントリスナを登録すると、スコープの外部から、現在選択中のボックスを取得するとなると、各ノードを走査して調べる事になりますが、これは美しくない。


Prior to this change, Prototype treated only the event names present in the Event.DOMEvents array as native DOM events. Now, Prototype looks for the presence of the namespace delimiter—a single colon—to determine whether you’re observing a custom event or a native event.(*1)

RC1の時点では、Event.DOMEventsの配列中の値を指定する事でNative Eventを呼び出せたみたいだけど、上記のChangesetを見る限り、1.6.0ではこれ無くなってるね。

で、Native DOM Eventを呼び出し可能な、Event.fireを再定義する事にした。
ボックスをクリックした時に呼び出されるイベントハンドラを、ボタンをクリックする事でも呼び出すようにしたサンプル。
Fire the native DOM Event


テスト

// A event listener
$('selection').observe('click', (function() {
var SELECTED_CLASS_NAME = 'selected';
var selected;
return function(event) {
if(selected) selected.removeClassName(SELECTED_CLASS_NAME);
event.target.addClassName(SELECTED_CLASS_NAME);
selected = event.target;
};
})());

// Fire the DOM Event dynamically
$('container').observe('click', function(event) {
if(event.target.tagName.toUpperCase() == 'INPUT') {
var choiceIdx = parseInt(event.target.id.replace('btn_', ''));
$('box' + choiceIdx).fire('click', {});
}
});



DOM 3 Eventならイベントオブジェクトを作成して、イベントを呼び出します。
Event.fireの第三引数には、Eventオブジェクト作成時のパラメータをハッシュ(Object)で指定できます。もし、カスタムイベントの場合は、通常のEvent.fireが呼び出され、Event.fireの第三引数として渡されます。最後に、Event#fireを新しく定義した、Event.fire_with_native_eventsで置換しておわり。

※今、手元にIEが無いので、IEでのテストが出来ていません。(あとで)

サンプル

$('text_field').fire('keydown', {charCode: Event.KEY_RETURN});


実装

Object.extend(Event, (function() {
// DOM Level 3 events
var W3C_MOUSE_EVENTS = $w('click mousedown mousemove mouseout mouseup');
var W3C_KEYBOARD_EVENTS = $w('keydown keyup keypress');
var W3C_BASIC_EVENTS = $w('abort change error load reset resize scroll submit unload');

function createDOMEvent(aEventName, aEventParams)
{
var event;
if(W3C_MOUSE_EVENTS.include(aEventName)) {
var p = Object.extend({
bubble: true,
cancelable: true,
view: window,
detail: 0,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
button: 0,
relatedTarget: null
}, aEventParams);
if(document.createEvent) {
event = document.createEvent('MouseEvent');
event.initMouseEvent(aEventName, p.bubble, p.cancelable, p.view, p.detail, p.screenX,
p.screenY, p.clientX, p.clientY, p.ctrlKey, p.altKey, p.shiftKey, p.metaKey,
p.button, p.relatedTarget);
} else {
// TODO: IE
Object.extend(event, p);
event.eventType = 'on' + aEventName;
}
} else if(W3C_KEYBOARD_EVENTS.include(aEventName)){
var p = Object.extend({
bubble: true,
cancelable: true,
view: null,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
keyCode: 0,
charCode: 0
}, aEventParams);
if(document.createEvent) {
event = document.createEvent('KeyboardEvent');
event.initKeyEvent(aEventName, p.canBubble, p.cancelable, p.view, p.ctrlkey, p.altKey, p.shiftKey, p.metaKey,
p.keyCode, p.charCode);
} else {
// TODO: IE
event = document.createEventObject();
Object.extend(event, p);
event.eventType = 'on' + aEventName;
}
} else if(W3C_BASIC_EVENTS.include(aEventName)) {
var p = Object.extend({
bubbles: true,
cancelable: true
}, aEventName);
if(document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent(aEventName, p.bubbles, p.cancelable);
} else {
// TODO: IE
event = doument.createEventObject();
Object.extend(event, p);
event.eventType = 'on' + aEventName;
}
}
return event;
}

return {
fire_with_native_events: function(element, eventName, eventParamsOrMemo) {
var event = createDOMEvent(eventName, eventParamsOrMemo);
if(event) {
document.createEvent? element.dispatchEvent(event) : element.fireEvent(event.eventType, event);
} else {
Event.fire(element, eventName, eventParamsOrMemo);
}
}
};
})());

// Replace an Element#fire
Element.addMethods({
fire: Event.fire_with_native_events
});


脚注
1. Prototype 1.6.0 RC1: Changes to the Class and Event APIs, Hash rewrite, and bug fixes

Sunday, January 13, 2008

[Javascript] クロージャを利用したイベントリスナの登録

時々、特定のイベント間で値を共有したい事があります。
以下の例は、選択されているボックスの状態をどこかに保存しておいて、別のボックスが選択されたら選択を取り消すサンプルです。選択されているエレメントの値を保存する場所としては、グローバル変数か、エレメントのプロパティ(IEで使用するためには、Element.extendされている必要がある。event.targetは、Element.extendされている。)がありますが、イベントリスナにクロージャーを利用すると名前空間も汚染されません。



Source Code
selected変数に、現在選択されているボックスをバインドしている。

document.observe('dom:loaded', function() {
$('selection').observe('click', (function() {
var SELECTED_CLASS_NAME = 'selected';
var selected;
return function(event) {
if(selected) selected.removeClassName(SELECTED_CLASS_NAME);
event.target.addClassName(SELECTED_CLASS_NAME);
selected = event.target;
};
})());
});

Tuesday, January 08, 2008

[Javascripts] Element.addMethodsByTag

This code requires prototype.js 1.6.x

特定のElementにメソッドを追加します。
以下の例は、canvasタグにキャンバス全体を初期化するメソッドを追加します。(HTMLCanvasElement#clear)


// Add arbitrary methods as HTML#{tag}Element instance methods
Element.addMethodsByTag = function(aTag, aMethods) {
if(aMethods.constructor == Object) {
var methods = new Object();
methods[aTag.toUpperCase()] = aMethods;
Object.extend(Element.Methods.ByTag, methods);
Element.addMethods();
}
};

Element.addMethodsByTag('canvas', {
clear: function(element) {
if(element.getContext) {
var ctx = element.getContext('2d');
ctx.clearRect(0, 0, element.getAttribute('width'), element.getAttribute('height'));
}
return element;
}
});



特定のHTML#{tag}Elementごとに、共有されるクラス変数を定義する事も出来ます。

Element.addMethodsByTag('canvas', (function() {
var someValue;
return {
// ...
};
}})());

[Javascript] Prototype.jsのEventオブジェクトを拡張する

prototype.js 1.6では、Element#observeで登録したイベントハンドラに渡されるEventオブジェクトが、Event.extendを呼び出したPrototype.js独自のEventオブジェクトで拡張されていますが、Eventオブジェクトに任意のメソッドを追加する方法です。

例えば、親ノードの座標からの相対座標を取得する、Event#relativeX, Event#relativeYをEventオブジェクトに追加する際は次のようになる。コードは単純だが、Event.addMethodsの引数に渡されるオブジェクトで定義されている要素が、eventを参照可能にするため、Event.extend内で再びメソッドを定義し直して、eventをスコープに加える必要がある。クロージャーを用いるメソッドの再定義は、関数のスコープを自由に変更できるので便利である。

※ Firefox 2.x, Safari 3で動作確認。IE未確認。


// Add arbitrary methods as instance methods of Event object
Event.addMethods = function(aMethods) {
var _extend = Event.extend;

Event.extend = function(event) {
var methods = Object.keys(aMethods).inject({}, function(m, name) {
Event.Methods[name] = aMethods[name];
m[name] = Event.Methods[name].methodize();
return m;
});
_extend(event);
Object.extend(Prototype.Browser.IE ? event : Event.prototype, methods);
return event;
};
};

// example
Event.addMethods({
relativeX: function(event) {
var origin = event.element().cumulativeOffset();
return event.pointerX() - origin[0];
},
relativeY: function(event) {
var origin = event.element().cumulativeOffset();
return event.pointerY() - origin[1];
}
});




イベントリスナ

Element.extend(node).observe('mousemove', function(event) {
console.log('pointerX: %d, pointerY: %d, relativeX: %d, relativeY: %d',
event.pointerX(), event.pointerY(), event.relativeX(), event.relativeY());
});

Saturday, December 08, 2007

[Javascript] iPhone / iPod touchの「slide to unlock」のエフェクト(Effect.Illuminate)を実装

iPhone / iPod touchのTOPに表示されている'slide to unlock'のエフェクトです。

Effect.illuminate




Usage

Effect.Illuminate('illuminated_msg');


Option

Effect.Illuminate('illminated_msg', {color: '#fff', repeat: true});

color: スポットライトの色です。
repeat: エフェクトを繰り返すかどうか。
size: スポットライトのサイズ(文字数)

Source code
テキストノード以外は、子ノードに対して再起的にエフェクトを適用し、テキストノードの場合は、スポットライトを照らす関数を遅延呼び出しで実行する。同時にオプションで指定した文字数以上照らした場合は、スポットライトを消す(移動)するエフェクトを開始する。エフェクトを実行し終えたら、innerHTMLの内容を元の状態に復帰する。


var Effect = Effect || {};
Effect.Illuminate = function(element, options) {
var effect = arguments.callee;
element = $(element);
if(element.tagName) {
// Save the original style
with(effect) {
color = element.getStyle('color');
innerHTML = element.innerHTML;
}
$A(element.childNodes).each(function(node) {effect(node, options)});
return;
}

// It's only executed when node is a text node.
options = new Hash({
color: '#ffffff',
size: 4,
repeat: true,
interval: 70
}).merge(options);

// The all characters is replaced because accessing an innerHTML property is pretty slow.
var parent = element.parentNode;
parent.innerHTML = $A(element.nodeValue).map(function(text) {
return ['<span style="color: ', this.color, '">', text, '</span>'].join('');
}).join('');

var self = this, started = false;
var turnOn = function(node) {node.style.color = options.color};
var turnOff = function(node) {node.style.color = self.color};
var restore = function() {parent.innerHTML = self.innerHTML};
var startTurnOffEffect = function(callback) {
started = true;
$A(parent.childNodes).inject(0, function(delay, node, idx) {
with({idx: idx}) {
setTimeout(function() {
turnOff(node);
if(callback['onEnd'] && idx == parent.childNodes.length - 1) callback['onEnd']();
}, delay);
}
return delay + options.interval;
});
};
// Start the effect
$A(parent.childNodes).inject(0, function(delay, node, idx) {
setTimeout(function() {
turnOn(node);
if(!started && idx > options.size) {
startTurnOffEffect({onEnd: function() {
restore();
if(options.repeat) effect(parent, options);
}});
}
}, delay);
return delay + options.interval;
});
};



iPhone or iPod touchユーザなら一目瞭然ですが、iPhone / iPod touchのエフェクトはより滑らか。またの機会に、イルミネーションをより滑らか(2〜3ステップで明るくなるよう)にするかもしれません。

Wednesday, November 14, 2007

[Javascript] 64ビット演算のエミュレート

「最近記事書いてないね」って突っ込まれる。
プログラムは、たくさん書いているのですが。

ECMA-262で定義されている通り、ビット演算子は符号付き32ビット整数しか扱えない。Number型は倍精度フォーマット IEEE 754型なので64ビットまで扱えるので、結果が2の31乗以上の値になるビット演算子を扱う時には2の32乗を加算して、補正してやる必要がある。

※ Math.pow関数など、2の53乗以上の数は誤差が出るので注意


console.log((Math.pow(2, 53)-1).toString(2)); // => 11111111111111111111111111111111111111111111111111111
console.log((Math.pow(2, 54)-1).toString(2)); // => 1000000000000000000000000000000000000000000000000000000


64ビット演算のエミュレート。
ECMAScript4にならないと演算子のオーバーロードができないので、関数呼び出しでエミュレートする。関数の呼び出しコストについては後述。


var ASSERT = function() { // {{{
var arg = new Array(arguments.length);
for(var i=0,len=arg.length;i<len;++i)i>[i] = arguments[i];
if('console' in window && 'assert' in console) { // use Assert of Firebug
console.assert(arg.shift(), arg.pop());
} else {
if(!arg.shift()) throw(arg.pop());
}
}; // }}}


// 4294967296 is Math.pow(2, 32);
Number.prototype.bitwiseAnd = function(rhs) {
return (this & rhs > -1)? this & rhs : this & rhs + 4294967296;
};
Number.prototype.bitwiseOr = function(rhs) {
return ((this | rhs) > -1)? this | rhs : (this | rhs) + 4294967296;
};
Number.prototype.bitwiseNot = function() {
return ((~this) > -1)? ~this : (~this) + 4294967296;
};
Number.prototype.bitwiseXor = function(rhs) {
return ((this ^ rhs) > -1)? this : (this ^ rhs) + 4294967296;
};

ASSERT(0xffffffff.bitwiseAnd(0xffffffff) == 1, 'bitwiseAnd: 0xffffffff & 0xffffffff is equal 4294967295.');
ASSERT(-0x1.bitwiseAnd(0xffffffff) == -1, 'bitwiseAnd: -0x1 & 0xffffffff is equal -1.');
ASSERT(0x1.bitwiseOr(0xffffffff) == 4294967295, 'bitwiseOr: 0x1 | 0xffffffff is equal 4294967295.');
ASSERT(-0x1.bitwiseOr(0xffffffff) == -4294967295, 'bitwiseOr: -0x1 | 0xffffffff is equal 4294967295.');
ASSERT(0x1.bitwiseXor(0xffffffff) == 4294967294, 'bitwiseXor: 0x1 ^ 0xffffffff is equal 4294967294.');
ASSERT(-0x1.bitwiseXor(0xffffffff) == -4294967294, 'bitwiseXor: 0x1 ^ 0xffffffff is equal -4294967294.');
ASSERT((~1) == -2, 'signed 32 bitwiseNot: (~1) is equal -2');
ASSERT(0x1.bitwiseNot() == 4294967294, 'bitwiseNot: 0x1 ^ 0xffffffff is equal 4294967294.');
ASSERT(-0x1.bitwiseNot() == -4294967294, 'bitwiseNot: 0x1 ^ 0xffffffff is equal -4294967294.');

delete ASSERT;



命令数を減らして高速化したいというニーズから、ビット演算を使用する事が多いかと思うので、上記の場合、関数呼び出しのコストが余分にかかる事に気をつける必要があります。インライン展開したコードに比べて、関数が呼び出されるコードでは、約2.5倍程遅くなるので必要に応じてインライン展開してください。

AND演算を100000回実行した結果
インライン: 387ms
bitwiseAnd関数: 991ms

Sunday, November 11, 2007

[Javascript] 64bit Number型を上位32bitと下位32bitに分割

JavascriptのNumber型は、倍精度64bitフォーマット IEEE 754型(double-precision 64-bit format IEEE 754 values)です。Javascriptでは、64ビット演算はできないので、上位32ビットと下位32ビットにわけて計算しないといけません。また、Math.pow(2, 55)など大きな数を扱う時には誤差も発生するので別途内部表現を設けて対応しないといけません。

パフォーマンスの犠牲を払っていますが、
上位32ビットと下位32ビットに変換します。


Number.prototype.rdHi = function() {
var b = this.toString(2);
return parseInt(b.slice(0,b.length-32), 2) || 0x0;
};
Number.prototype.rdLo = function() {
var b = this.toString(2);
return parseInt(b.slice(Math.max(b.length-32, 0), b.length), 2);
};
console.log(0x03ffffffff.rdHi() & 0xffffffff); // => 3
console.log(0xffffffffff.rdHi() & 0xffffffff); // => 255
console.log(0x01ffffffff.rdLo()); // => 4294967295
console.log(0x01ffffffff.rdLo() & 0xffffffff); // => -1


注意しなければいけないのは、最上位ビットは符号ビットだと言う事。最後の結果は、0xffffffffではなく、-1になります。32bit値を何かしらのフラグとして利用する場合は、最上位ビットを考慮したロジックを組む必要があります。

Number型(64bit)を利用する場合速度とメモリ消費量のトレードオフとなります。
例を挙げれば、ボードゲームを実装する際のデータ構造として一般的なBit Boardならば、次のどちらかの戦略をとる事になるかもしれません。
  • 64bit値を下位32bitと上位32bitの二要素として配列に持つ
  • 64bit変数で保持して、実行時に上記のメソッドを適用する。

一つ目は、メモリ消費量が多くなりますが配列から何かしらの操作を実行する時のコストは低くなり、二つ目はメモり消費量は減らせるが実行時のコストが大きくなります。

P.S
2の53乗以上のデータも扱えるように、倍精度でも誤差がでないNumber型を実装していたら似たようなライブラリがたくさんあったので中断。きちんと調べてから実装するべきだった。

Sunday, April 08, 2007

[ruby] 特異メソッドに別名をつけて退避する

特異メソッドをalias_methodを使用して別名をつけようとするとエラーになる。
だが、同じブロック内でrespond_to?(:find)を実行するとtrueが返される。


undefined method `find' for class `ActiveRecord::Base' (NameError)



module EffectiveRails
module ActiveRecord
def self.included(klass)
klass.send ClassMethods
klass.class_eval do
p respond_to?(:find) # => true
alias_method_chain :find, :explain
end
end

module ClassMethods
def find_with_explain(*args)
# hogehoge
find_without_explain(*args)
end
end
end
end

ActiveRecord::Base.class_eval do
include EffectiveRails::ActiveRecord
end



Rubyの少々ややこしいクラスとオブジェクトの仕組みによるものだが、特異メソッドに別名をつけて退避したい場合は、alias_method (_chain)メソッドではなくてaliasをinstance_evalのブロック内で使用するとよい。


module EffectiveRails
module ActiveRecord
def self.included(klass)
klass.send ClassMethods
klass.instance_eval do
alias :find_without_explain :find
alias :find :find_with_explain
end
end

module ClassMethods
def find_with_explain(*args)
# hogehoge
find_without_explain(*args)
end
end
end
end

ActiveRecord::Base.class_eval do
include EffectiveRails::ActiveRecord
end



同じような現象でハマッた方も多いのではないだろうか?