l'essentiel est invisible pour les yeux

Saturday, January 19, 2008

[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();
});