圧縮

「001111111111555555555599944578888」
とある案件でこのようなコードの桁数を短くする要望が発生してしまった。
ここは「圧縮」である。
「圧縮」でイメージするのはjpegやlzhなど。敷居が高く感じる。だが調べてみると思ったほど難しく無かった。

中でもランレングス圧縮という方法はわかりやすくすぐ実装できそうだ。
早速エディタを起動しキーボードを叩き始める。

ランレングスはざっくりいうと「同じデータはまとめてしまえ」という手法である。
「11111」のように数値の「1」が5回続いた場合は115( 1を2回書くことで2回以上続いていることを表す)のように記述する。
もっとも2回しか連続してしない場合は3文字使ってしまうので、元のデータより桁数は増えてしまう。
今回のコードは比較的同じ数値が連続することが多いので効果が期待できそう。

実装を終えテストしてみるとなかなかいい具合ではないか。
もう少し短くするため36進数変換し、さらに何箇所かにハイフンを入れる。これで大分見やすくなった。

今回はactionScript案件ではあったが、javascriptに移植してみた。
以下URLにてテスト可能なので興味があったらお試しを。
 http://nouv.biz/labo/20140810_runlength/RunLengthTest.html


フレームレートとパフォーマンスモニタについて

フレームレートとは元々は映像で用いられる言葉である。
アニメーションなどで1秒間に何コマ絵を動かしますか、という意味だ。1秒間に24コマの場合は24FPS(フレームパーセコンド)などと表される。FlashやJavaScriptなどで動作させるアニメーションでも用いられることが多い。

このフレームレート、Flashの場合直接フレームレートの指定が可能(デフォルトは12)だが、Javascriptの場合はtimer(setTimeoutなど)で逆算して指定することになる。
timerはブラウザにより大きく性能が異なるため、ゲーム等を制作する場合大きな障害となる。例えばキャラクタが画面を移動するとき、あるブラウザでは0.5秒かかり、別のブラウザでは2秒かかるなど、差異が大きすぎるとゲームとして成立しない。

この問題はオライリー・ジャパンの「JavaScriptグラフィックス」では以下の方法で対処している。

  1. どの程度のフレームレートが実現しているか調べる
  2. 実現したい速度と実際の速度の比率(=係数)を調べる
  3. 1フレームあたりのキャラクターの移動距離を標準の距離×係数で再設定する
  4. フレーム毎にキャラクターを再設定した距離移動させる

一方Flashサイトの場合、ゲーム用途よりバナーやトップページフラッシュなど演出目的のものが多いため、滑らかな動作が重要視される。滑らかに動かした方がより高級感を醸し出せるからだ。
滑らかに動かすためには、アニメーションは1秒のコマ数を増やせばよい。そこで多くの製作者は極力高いフレームレートを設定することになる。

ところが高くすればするほどパソコンに負荷がかかり、スペックの低いパソコンでは設定したフレームレートの半分も出せないことがある。結果カクカクした動きになっしまう。

その対策については、こちらもオライリー・ジャパンの「Flash Hacks」にわかりやすく記載されている。
まず上記1同様、「どの程度のフレームレートが実現しているか」を調べ、オブジェクトが多数動くことが負担になっている場合は、動作しているオブジェクト(キャラクターや粒子など)の数を減らしている。
上記書籍は2005年発売でActionScript1.0又は2.0で記述されているので、今回はActionScript3.0に置き換えつつ、より実践的にしたPerformanceMonitorクラスを作成した。
以下がそのサンプルとなる(http://nouv.biz/labo/20121004_performance_montor/)。
大量のパーティクル(粒子)を生成し、ランダムにY回転させている(重いので他作業中の方は注意)。

右下に、実現しているフレームレートと、パーティクルの数を表示している。
フレームレートは24FPS、パーティクルの初期生成数は10万個を初期値にしているが、フレームレートが24フレームに達しないと、序々にパーティクルの個数を減らし調整する。

フレームレートが目標に達しているか調査する部分のスクリプトを以下に記載。

  private function onEnterframe(event:Event):void {
   var nowTime:Number = new Date().getTime();
   var pastTime = nowTime-oldTime ; 
   var fps:Number = 1000 / pastTime;

   if (interCount > 100) {
    //過去の累積が悪影響をおよぼすので、ある程度の回数でクリアする
     totalFPS = 0;
     interCount = 0;
   }
   totalFPS += fps;
   interCount++;
   oldTime = nowTime;
  }

  /*
   * 
   */
  public function get _averageFps():int {
   var averageFPS:Number = Math.floor(totalFPS / interCount );
   return averageFPS;
  }

  /**
   *達成したいフレームレートを指定する
   * 達成できているかどうかを返す
   */
  public function fpsBool(targetFPS:int):Boolean {
   var bool:Boolean;
   if (_averageFps >= targetFPS) {
    bool = true;
   }else {
    bool = false;
   }
   return bool;
  }

現時刻から前に通過した時刻を引き、そこからフレームレートを求めその値を返す。
一方、パーティクルはその結果を受けて個数調整を行なっている。
numBallsは生成するパーティクルの個数。上限下限を設けている。

	
performanceBool = performanceMonitor.fpsBool(24);
if (!performanceBool) {
	numBalls = (numBalls - 100) > 0? numBalls - 100 : numBalls;
}else {
	numBalls = (numBalls + 100) <= makeNum ? numBalls + 100 : makeNum;
}

 

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