l'essentiel est invisible pour les yeux

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