Lightbox.js で学ぶ script.aculo.us アニメーションの使い方

写真をWEBで見せるためには、Javascriptでのエフェクトが流行な昨今ですが、Lightbox.js のような見せ方をどうやって実現しているのか興味があったので、ちょっとLightbox.jsを題材に勉強してみようかと、遅ればせながら思い立ってみたり。。

lightbox クラスのinitialize

早速、Javascript のソースをざっくり見てみる。まずは、prototype.js の作法に則ってクラスを作成ですね。

var Lightbox = Class.createl(); 

続けて、Lightbox クラスのイニシャライズ

Lightbox.prototype = {}

このあたりは、lightbox.js というよりprototyppe.js のお話になりますね。以下、参考文献。

イニシャライズの具体的な処理は、以下の中で記述されていて

initialize: function() {}

処理の内容としては、まずは、ドキュメントの中から<a>タグをすべて抜き出す。

var anchors = document.getElementsByTagName('a');

次に、抜き出した<a>タグの中から、rel に lightbox が指定されているものタグの href属性に、以下のjavascriptのfunctionを指定する

anchor.onclick = function () {myLightbox.start(this); return false;}

続いて226行から312行までは、DOM操作で以下のHTMLタグを、HTMLページの一番下に埋め込む。このHTMLは、実際にLightboxで写真を表示する際の枠組みになるHTML。lightbox.js というライブラリ形式で提供するために、延々とDOM操作で入れ込んでいるが、自分でHTMLとJavascriptを全部書くのであれば、HTMLに記述しておいて、CSSでdisplay:none; に指定しておいても問題ない。

initialize の次は、Lightbox クラスのファンクションの定義。Lightboxは以下のようなファンクションを持っている。 説明をソース内のコメントから抜き出すと

  • initialize()
    クラスのイニシャライズ処理
  • start()
    ブラウザの画面上にオーバーレイでlightboxを表示。画像がセットの一部であれば、セットの画像を配列に追加する
  • changeImage()
    ほとんどのElementを非表示にして、画像コンテナのリサイズのために、画像を先読みする。
  • resizeImageContainer()
  • showImage()
    画像を表示して、次の画像を先読みする
  • updateDetails()
    キャプションや画像の番号、下のナビゲーションを表示する
  • updateNav()
    画像の上にHover表示する、Next / Previous のナビゲーションをアップデートする
  • enableKeyboardNav()
    キーボード入力でナビゲーションするためのキー操作イベント設定
  • disableKeyboardNav()
  • keyboardNavAction()
  • preloadNeighborImages()
    次と前の画像を先読み込みする
  • end()
    終了処理

start()

Initialize が終わったあとに、次に実行されるのは、start() ファンクションになります。

start: function(imageLink) {}

start() は、initialize の処理の一環で、<a>タグ の onclick にアタッチしたfunction。引数 imagelink は、anchor自身を示す this になります。

start() の中で一番最初におこなっている処理は、hideSelectBoxes(); で<select>を非表示にしている。ここで非表示にした<select>ボックスは、最後の処理end() の中で再度、表示しているのだけど、ひとまずその意図は分かららないのでスキップ。次に行っているのが、画面全体にオーバーレイをかける処理。

// stretch overlay to fill page and fade in
var arrayPageSize = getPageSize();
Element.setHeight('overlay', arrayPageSize[1]);
new Effect.Appear('overlay', { duration: 0.2, from: 0.0, to: 0.8 });

ブラウザの画面サイズを取得する、 getPageSize() というファンクションは、quirksmode.org というサイトで公開されていたものを利用しているとコメント内に書いてあるのですが、そのサイトはクローズはしてしまったよう。ひとまず、このライブラリ・ファンクションを利用すると、Array(pageWidth,pageHeight,windowWidth,windowHeight) という配列を返してくれます。

この画面サイズの情報を利用して、前述のlightbox を表示するHTMLの一番外側の<div id="overlay">の高さを設定します。高さを設定するElement.setHeight() というファンクションは、prototype.js のObject.extendを使った拡張機能ライブラリとして定義されています。こちらも外部のライブラリ。他にも setTop(), setHref() などがありますが、基本的にはその名の通り、idを指定したエレメントのプロパティを指定するシンプルなファンクション。

Effect.Appear はscript.aculo.usでフェードイン表示するファンクション。

Effect.Appear in scriptaculous wiki

その次に、もう一度、aタグをすべてgetElementByTagNameで取得して、クリックされた画像リンク(imagelink)と同じrelが指定されたaタグを、imageArray[] に追加する。そのあとにに、imageArray.removeDuplicates();というファンクション(定義は Array.prototype.removeDuplicates()でされている)で重複を削除したり、imageNum という変数に画像の枚数を指定したりする。

imageArray = [];
imageNum = 0;		

if (!document.getElementsByTagName){ return; }
var anchors = document.getElementsByTagName('a');

// if image is NOT part of a set..
if((imageLink.getAttribute('rel') == 'lightbox')){
  // add single image to imageArray
  imageArray.push(new Array(imageLink.getAttribute('href'), imageLink.getAttribute('title')));			
} else {
// if image is part of a set..
// loop through anchors, find other images in set, and add them to imageArray
  for (var i=0; i<anchors.length; i++){
    var anchor = anchors[i];
    if (anchor.getAttribute('href') && (anchor.getAttribute('rel') == imageLink.getAttribute('rel'))){
      imageArray.push(new Array(anchor.getAttribute('href'), anchor.getAttribute('title')));
    }
  }
  imageArray.removeDuplicates();
  while(imageArray[imageNum][0] != imageLink.getAttribute('href')) { imageNum++;}
}

imageArray[]の取得が終わったら、<div id="lightbox"> の場所を指定して表示。

// calculate top offset for the lightbox and display 
var arrayPageSize = getPageSize();
var arrayPageScroll = getPageScroll();
var lightboxTop = arrayPageScroll[1] + (arrayPageSize[3] / 15);

Element.setTop('lightbox', lightboxTop);
Element.show('lightbox');

そして処理は、this.changeImage(imageNum); へと続きます

changeImage()

changeImage() は、表示する画像の先読みをおこないます。まずは引数でとった imageNumを、global var に設定。

activeImage = imageNum;	// update global var

次に、Element.show('loading'); でloadingアイコンを表示して、その他のElementは非表示に。そしてメインの画像の先読みはこんな感じで実装されています。

imgPreloader = new Image();

// once image is preloaded, resize image container
imgPreloader.onload=function(){
  Element.setSrc('lightboxImage', imageArray[activeImage][0]);
  myLightbox.resizeImageContainer(imgPreloader.width, imgPreloader.height);
}
imgPreloader.src = imageArray[activeImage][0];

Imageオブジェクトを作って、そのオブジェクトのonloadにロード後の処理を記述。その後にImageオブジェクトのsrcに、activeImageの画像URLを指定してロード&処理を実行させると。なるほど。処理は resizeImageContainer() に引き継がれます。

resizeImageContainer()

このファンクションは、先読みしたImageオブジェクトである imgPreloader 高さと幅を、imgWidthとimgHeightという引数にとって、画像の表示枠のサイズを、先読み画像のサイズに合わせてアニメーションでリサイズするという処理。画像の表示の枠は、<div id="outerImageContainer">というHTMLタグで記述されている。実際のリサイズ処理の内容は、ソースコードそのままという感じであります。

// get current height and width
this.wCur = Element.getWidth('outerImageContainer');
this.hCur = Element.getHeight('outerImageContainer');

// scalars based on change from old to new
this.xScale = ((imgWidth  + (borderSize * 2)) / this.wCur) * 100;
this.yScale = ((imgHeight  + (borderSize * 2)) / this.hCur) * 100;

// calculate size difference between new and old image, and resize if necessary
wDiff = (this.wCur - borderSize * 2) - imgWidth;
hDiff = (this.hCur - borderSize * 2) - imgHeight;

if(!( hDiff == 0)){ new Effect.Scale('outerImageContainer', this.yScale, 
  {scaleX: false, duration: resizeDuration, queue: 'front'}); }
if(!( wDiff == 0)){ new Effect.Scale('outerImageContainer', this.xScale, 
  {scaleY: false, delay: resizeDuration, duration: resizeDuration}); }

// if new and old image are same size and no scaling transition is necessary, 
// do a quick pause to prevent image flicker.
if((hDiff == 0) && (wDiff == 0)){
  if (navigator.appVersion.indexOf("MSIE")!=-1){ pause(250); } else { pause(100);} 
}

Element.setHeight('prevLink', imgHeight);
Element.setHeight('nextLink', imgHeight);
Element.setWidth( 'imageDataContainer', imgWidth + (borderSize * 2));

this.showImage();

scriptaculousの、Effect.Scale のオプションで渡されている、 queue: 'front' ってなんだろう? Effect.Scale のドキュメントには特に記載が無かったので検索。

上記のページで詳しく解説されていますが、scriptaculousは、タスクを順番に実行するためのQueuingの仕組みをもっていて、そのQueに追加するためのオプションのようです。Queは基本的は一つのGlobalな待ち行列になっていて、'end'はqueの最後に追加、'front'は、queの最初に追加するそう。上記のページのサンプルで

new Effect.SlideUp('test1', {queue: 'end'});
new Effect.SlideDown('test1', {queue: 'front'});

と記述されていると、SlideDownをしてから、SlideUpが実行されます。LightBoxで使っている理由は、画像表示枠のアニメーションが、縦に動いてから横に動くように、実行の順番を保証するためのよう。それ以降の処理は、prevLink などナビゲーションの表示位置の設定ですね。そして、実際に画像を表示する、showImage() に続く。

showImage() は、loadingを隠して、画像を表示するだけのシンプルなファンクションですが、Effect.Apprear でafter:Finish というオプションを使っている。別のファンクションの実行が終わった後に、このエフェクトを実行するというオプションですね。

Element.hide('loading');
new Effect.Appear('lightboxImage', 
  { duration: 0.5, queue: 'end', 
    afterFinish: function(){myLightbox.updateDetails(); } 
  });
this.preloadNeighborImages();

こういうオプションって、ドキュメントのどこにかいてあるのかな〜と探したら、前述のqueueも含めてのこのページにありました。結構いろんなcallbackがあるんですね。

updateDetails() / preloadNeighborImages()

updateDetail() では、画像の名前や順番等の情報を表示。目新しいものとしては、Effect.Parallel() ですかね。複数のエフェクトを配列で渡すことで、それらを同時に実行してくれます。

new Effect.Parallel(
[ 
  new Effect.SlideDown( 'imageDataContainer', 
    { sync: true, duration: resizeDuration + 0.25, from: 0.0, to: 1.0 }), 
  new Effect.Appear('imageDataContainer', { sync: true, duration: 1.0 }) 
], { duration: 0.65, afterFinish: function() { myLightbox.updateNav();} } 
);

preloadNeighborImages() では、現在、表示している画像の前と次を先読みしています。

if((imageArray.length - 1) > activeImage){
  preloadNextImage = new Image();
  preloadNextImage.src = imageArray[activeImage + 1][0];
}
if(activeImage > 0){
  preloadPrevImage = new Image();
  preloadPrevImage.src = imageArray[activeImage - 1][0];
}

あとは、キーボードでナビゲーションができる、keyboardAction: function(e) {} などもありますが、今回はひとまず割愛。最後に、end: function() {} で、Lightboxを非表示にしつつ、overlayをフェードアウトさせて終了。

最後に

ファイルの一番最後には、Lightbox インスタンスを作成する initLightbox() と、それをwindowがロードされたときに実行する Event.observe が記述されています。

function initLightbox() { myLightbox = new Lightbox(); }
Event.observe(window, 'load', initLightbox, false);

以上、ざっくりとLightboxの流れを追ってみましたが、ライブラリとDom操作の部分を除くと、意外とシンプル&そんなに長くなくて、script.aculo.usのいい勉強になった感じです。Lightboxのように、どんなHTMLにも組み込めるライブラリとしてでなく、自分の使う用途に絞れば、さらにシンプルにカスタマイズしやすくできるかもしれませんね。

lightbox.js とPHPで手軽にフォト・アルバムを作成:Goodpic