JavaScriptのクラス記述方式(継承)

前回作成したDHTMLSpriteクラスを継承した、bouncySpriteクラスを作成する。
関数を用いたクラス生成方式の場合、継承は以下のとおりとてもわかりやすい。

var bouncySprite=function(params){
	var x=params.x;
	var y=params.y;
	var xDir=params.xDir;
	var yDir=params.yDir;
	var maxX=params.maxX;
	var maxY=params.maxY;
	var animIndex=0;
	var that=DHTMLSprite(params);
	that.moveAndDraw=function(){
		x+=xDir;
		y+=yDir;
		animIndex+=xDir>0 ? 1:-1;
		//進行方向に合わせて歯車の回転も変える。
		animIndex%=5;
		//1行4列 5の余りをx軸(列数)とする
		animIndex+=animIndex0 && x>=maxX)){
			xDir=-xDir;
		}
		if( (yDir0 && y>=maxY)){
			yDir=-yDir;
		}
		that.changeImage(animIndex);
		that.draw(x , y);
	}

	return that;
}

使うプロパティと追加のプロパティを自分で引き受け、thatとしてDHTMLSpriteのインスタンスをthatとして生成する。そして、そのthatに新たなメソッドを付け加える。

一方、パターン2の教科書的方法は少々ややこしい(まだバグ無くならず)。こちらを参照。

2012.9.12 バグ解消。全文記載

function BouncySprite(params){
	//新たに追加されたプロパティを記述する
	this.x=params.x;
	this.y=params.y;
	this.xDir=params.xDir;
	this.yDir=params.yDir;
	this.maxX=params.maxX;
	this.maxY=params.maxY;
	this.animIndex=0;
	DHTMLSprite.call(this , params);
	//親のオブジェクトのコンストラクタを呼び出す
}
BouncySprite.prototype=new DHTMLSprite();
BouncySprite.prototype.moveAndDraw=function(xPos , yPos){
	with(this){
		xPos+=xDir;
		yPos+=yDir;
		animIndex+=xDir>0 ? 1:-1 ; 
		//進行方向に合わせて歯車の回転方向を変える
		animIndex%5; 
		//1行4列 5の余りをx軸(列数とする)	
		animIndex+=animIndex0 && xPos>maxX)){
			xDir=-xDir;	
		}
		if( (yDir0 && yPos>=maxY)   ){
			yDir=-yDir;
		}
		changeImage(animIndex)	;
		draw(xPos, yPos);
	}
}

継承は関数形式の方がはるかに使いやすそう。

JavaScriptのクラス記述方式

数冊JavaScript本を読んだところ、クラス(もどき)の記述方法が数パターンあるみたいなのでまとめておく。
サンプルとしては前回同様オライリーの「JavaScriptグラフィックス」で用いられているスプライト描画を使わせていただきました。

パターンその1:
関数を用いる方法。「JavaScriptグラフィックス」で推奨されている方法である。

myClass=function(引数){
 that={
  xxx.....
 };
 return that;
}
var インスタンス=myClass(引数)

http://www.oreilly.co.jp/pub/9784873115283/ch02/ex0201.html
パターン1のソースについてはオライリーの書籍と上記URLを参照いただきたい。

パターンその2:
「教科書的」と言われている方法。
クラス風にインスタンスを生成する。

var インスタンス=new MyClass( 引数 )

それではパターン1の関数を用いる方式から。
サンプルの流れは以下の通り
1.htmlでid指定されているエリア

<div id="draw-target"></div>

を取得し、背景画像にcssで幅・高さ・該当画像(スプライトシート:(いくつかの画像が固定ピクセル毎に行と列で描かれているもの)を指定する

	
var DHTMLSprite=function(params){
	var width=params.width;
	var height=params.height;
	var imagesWidth=params.imagesWidth;
	//html記載 id:draw-targetにdivを追加。
	//そのdiv(即ち今追加したdiv)を$element変数に格納する
	var $element=  params.$drawTarget.append('<div/>').find(':last');
	$element.css({
		position:'absolute',
		width:width , 	
		height:height,
		backgroundImage:'url('+params.images+ ')'
	});
	var elemStyle=$element[0].style;//$elementのスタイルシートを格納。

2.スプライトシート中の何番目の画像を使うか指定するメソッドを作る

changeImage:function(index){
	index*=width;
	var vOffset=-mathFloor(index/imagesWidth)*height;//y座標
	var hOffset=-index%imagesWidth; //x座標
	elemStyle.backgroundPosition=hOffset+'px' + ' '+ vOffset +'px';
} ,

3.描画の際の座標を指定するメソッドを作る

darw:function(x , y){
	elemStyle.left=x+'px';
	elemStyle.top=y+'px';
},

生成は以下のとおり関数の受けで行う。

		var sprite1=DHTMLSprite(params);		
		var sprite2=DHTMLSprite(params);

サンプルではスプライトシート1つめ(指定していない場合デフォルト1つめ)の歯車の画像が座標x=64 y=64に、5つめのグラデーション画像がx=352 y=192に、それぞれ描画されている。

		sprite2.changeImage(5);
		sprite1.draw(64 , 64);
		sprite2.draw(352 , 192);

一方、パターン2の教科書的方法は少々ややこしい。DHTMLSpriteクラスを作ってみた。全文記載。

function DHTMLSprite(params){
	this.width=params.width;
	this.height=params.height;
	this.imagesWidth=params.imagesWidth;
	this.$element=params.$drawTarget.append("<div/>").find(":last");
	this.$element.css(
		{
			position:"absolute" , 
			width:this.width , 
			height:this.height,
			backgroundImage:"url("+ params.images + ")"
		}
	)
	this.elemStyle=this.$element[0].style;
	//要素を引数にしたjQueryオブジェクトは
	//DOM要素が配列で格納されている状態なので
	//$('#xxxx')[0]でDOM要素の参照となる。
	this.mathFloor=Math.floor;
 //高速化のためMath.floorのローカル参照を持つ

}
DHTMLSprite.prototype.draw=function(x , y){
	with(this){
		elemStyle.left=x+"px";
		elemStyle.top=y+"px";		
	}

}
DHTMLSprite.prototype.changeImage=function(index){
	with(this){
		index*=width;
		var vOffset=-mathFloor(index/imagesWidth)*height ; 
			//y座標
		var hOffset=-index%imagesWidth ; 
			//x座標
		elemStyle.backgroundPosition
			=hOffset+'px' + " "+vOffset+'px';
	}
}
DHTMLSprite.prototype.show=function(){
	with(this){
		elemStyle.display="block";
	}
}

DHTMLSprite.prototype.hide=function(){
	with(this){
		elemStyle.display="none"
	}
}
DHTMLSprite.prototype.destroy=function(){
	with(this){
		$element.remove();
	}
}

生成は

		var sprite1=new DHTMLSprite(params);		
		var sprite2=new DHTMLSprite(params);

で行う。描画の指定方法は変わらない。
thisが多いためやや汚く見えるが、生成がActionScript風なのでこちらの方が好みではある。次回は継承についてまとめる。

jQueryとDOM操作の最適化

前回同様、O’REILLYの「JavaScriptグラフィックス」1章効率化についてメモ。

キモは

  1. 「要素が見つかったら極力再検索しない」
  2. 「要素の追加は、文字列で一括して行う」

1.要素の検索について

$('#elem1').css('color' , '#ff0000');//赤
$('#elem1').css('color' , '#00FF00');//緑
$('#elem1').css('color' , '#0000FF');//青
$('#elem1').css('left' , '100px');//x座標を動かす

このサンプルではelem1を毎回検索しているので、効率が悪い。
そこで検索結果を変数に保存する。

$alien=$('#elem1');

となる。

※ここまでは想像していたのだが、この章の説明は検索範囲の指定まで踏み込んでいる。
曰く「jQueryが検索する要素の範囲を指定したほうが速い」。
$alien=$(‘#element1′ , container);//特定のDOM要素から検索する
個人的にはあまり使わないような気がするが一応覚えておく。

まだjQueryのcss()関数が残っている。これも変数に格納。

var elemStyle=$('#elem1')[0].style;

そしてcssスタイルを設定する。

elemStyle.color='#ff0000';
elemStyle.color='#00ff00';
elemStyle.color='#0000ff';
elemStyle.left='100px';

(なぜかelemStyle.colorをアラート表示できない)

2.要素の追加は文字列で一括して行う
DOMへの要素挿入は遅いので、

文字列+=追加文字列
で文字列を作成し、
$elem.append(文字列);
で一気に追加する。

これらを踏まえたサンプル。
http://nouv.biz/labo/20120908_jQuery_speed/ex0100_css.html

css操作はcssが設定されていないと動作しないので注意。コードは以下のとおり。

[html]
<div id=”element1”><div>
[css]
#element1{
	position:absolute;
	left:0;
	top:0;
	font-size:80px;
	color: #000000;
}
[JavaScript]
var $elem;//jQueryから取得
var elemStyle;//取得した要素のスタイル
var divs='';//追加する要素

//検索し、変数に格納 idを取得しているのに、配列のようなものになるらしい
$elem=$('#element1');
//$elem[0].addEventListener("click" , onClicked , false);
AddEvent.addEvent($elem[0] , "click" , onClicked);//オリジナルクラス

//変数からstyleを格納
//var elemStyle=$elem.style;
//Debugger.log(elemStyle);//undefinedになる
var elemStyle=$elem[0].style;//なぜか添字0をつけなければならない。
Debugger.log(elemStyle);

//makeDivs
//makeDivs
for(var i=0 ; i divs+=''//文字列追加省略
}

function onClicked(event){
Debugger.log("clicked");
elemStyle.color="#ff0000";
elemStyle.left="50px";
elemStyle.top="50px";
$elem.append(divs);
}

ルックアップテーブルとビット演算

O’REILLYの「JavaScriptグラフィックス」1章効率化についてメモ。

O’REILLYの書籍サイトではサンプルとして以下が掲載されている。
http://www.oreilly.co.jp/pub/9784873115283/ch01/ex0100.html
サインカーブを描くシンプルなもの。

特徴は

  • Canvasを用いず、ブロック要素(div)をhtml側に記述しcssで幅・高さを指定
  • ブロック要素内に幅1pxの縦線を幅の分の数描画する(この場合480本)
  • 描画の開始位置と高さはJavaScriptで変動させアニメーション化する

効率化のメインは

  1. ルックアップテーブル
  2. ビット演算

の2つ。
1つ目のルックアップテーブルは、都度sinを計算するのではなく、前もってsinを計算し、配列に格納することにより速度向上を図るということ。wonderFL(flash)でもスクリプトで絵柄をビットマップ描画し、それを配列に格納することで超高速化していたサンプルがあった(http://wonderfl.net/c/c0Gi)。

一方、2つ目のビット演算は少々わかりづらい。
なんの前置きも無く、

        drawGraph(x * 50, 32 - (sinTable[(x * 20) & 4095] * 16),
                  50 - (sinTable[(x * 10) & 4095] * 20));

とか

bars[i].style.top = 160 - height + 
                 sinTable[(ang + (i * freq)) & 4095] * height + 'px';

など唐突に4095という数字が出てくる。
結局
配列の生成数4096は16の3乗、0×1000
配列範囲(添字)の最大値 4095は16の3乗-1    0xFFF
ということにさえ気づければなんということは無かったのだけれど。

このサンプルでは360度(Math.2PI)を4096分割し、そのsin値を配列に格納している。
タイマー内の変数xを単純な足し算(x++)でどんどん増やしているため、このxから0~4095(配列範囲)の添字を算出する必要がある。そこでビット単位のAND(&)計算を用い
0×1000 (=4096)
0x  FFF(=4095)        &
———————————–
0X  000(=0 配列の先頭の添字)
などと計算し、範囲溢れを防ぎつつ添字を求めている。
私はこういう場合、余り(%、MOD)を求めて算出するのが通常だった。
余りで計算する場合、配列数(length=4096)を用いる。
上記の例だと
4096%4096=0 余り0 配列の先頭
4097&4096=0 余り1 配列の2つ目
となる。

ところがこの「余り」の計算は四則演算の中で最も遅いらしい
(参考にさせていただきました http://ufcpp.net/study/algorithm/col_circular.html) 。

このサンプルでは高速化のため、これをAND演算に切り替えている。
なるほど。
確かにカーブを書く都度「余り」計算を行なっているので計算回数も多い。ここの高速化は全体への影響も大きいだろう。
これを流用したサンプルはまた後日。

「ActionScript3.0アニメーション」をcanvasで置き換える 引き合うパーティクル 応用(メタボール)

先日作った「引きあうパーティクル」のFlash側を少し応用し、メタボール表示してみる。
メタボールとは何か?見てもらったほうが早い。
http://nouv.biz/labo/20120902_nodeGardenMetaBall/NodeGardenMetaballTest.html

1.中央から周辺へ、非透明→透明のグラデーションのボールを描画
2.パレットマップ用の配列を作成(透明度200以下は全て透明それ以上は全て非透明の配列を作成)
var separateNum:int = 200 //どこでグラデーションの切れ目とするか=metaのレベルとともにballの大きさにも影響する
alphas = new Array();
//200まではすべて透明
for (i = 0 ; i < separateNum ; i++) {
alphas.push(0);
}
//200以上は全てffになる。単独では200以下で透明でも、ふたつのグラデーションが重なって200以上になると非透明に→メタボール状態になる
for (i = separateNum ; i < 256 ; i++) {
alphas.push(0xFF000000);
}
このalphasをパレットマップに適用する。
bmd.paletteMap(bmd , bmd.rect , bmd.rect.topLeft , null , null , null , alphas);

外周付近が半透明な円が2つ以上重なり透明度が200を超えた場合、真っ黒に色が変換される。結果円が繋がったように見える。
メタボールについては webDesigning 2006年12月号に掲載されたFlashPlayer8向けの記事を参考にさせていただいた。6年近く前の記事なのにいまだ色褪せない。
梅津岳城氏に感謝。