datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【Wonderfl移植計画】yawarakaBall【1作目】

最近javascriptを触ってないので、今回からwonderflのFavoriteTop100にランクインしている作品をCreateJSを使って移植しようと思います。


ライセンスはMITなのを確認していますが、制作者様の方から何かアクションがあった場合は削除させていただきます。

86位 YawarakaBalls


移植元のFlashソースコード

ソースコードは次回から解説をします。


今回はソースコードを掲載するだけにします。

デモ

// EaselJS 0.7.0

// 読み込み終了時に初期化関数を実行
window.onload = init;

var SCREEN_WIDTH;
var SCREEN_HEIGHT;
var FLOOR_Y;
var stage;
var floor = new createjs.Container();
var space = new createjs.Container();

// Ballクラス
(function()
{
	Ball.prototype.matrix = new createjs.Matrix2D();

	// コンストラクタ
	function Ball(radius, color)
	{
		this.radius = radius;
		// 速度
		this.vx = randomRange(-1.5, 1.5);
		this.vy = randomRange(0, 1.5);
		// 変形のための接触位置
		this.cx = 0;
		this.cy = 0;
		this.priority = 0;
		// 位置をランダムに決定
		this.x = randomRange(radius, SCREEN_WIDTH - radius);
		this.y = randomRange(50, FLOOR_Y - radius);

		this.body = new createjs.Shape();
		var gr = this.body.graphics;
		// 半透明の円の描画
		var r = (color >> 16) & 0xff;
		var g = (color >> 8) & 0xff;
		var b = (color) & 0xff;
		gr.beginRadialGradientFill(
			[createjs.Graphics.getRGB(r, g, b, 0.9), createjs.Graphics.getRGB(r, g, b, 0.8), createjs.Graphics.getRGB(r, g, b, 0.4)],
			[0.0, 0.9, 1.0], this.cx, this.cy, 0, this.cx, this.cy, radius)
		.drawCircle(0, 0, radius);

		// CreateJSにはSpriteに既にshadowプロパティが用意されているので、異なるプロパティを用意する
		this.floorShadow = new createjs.Shape();
		gr = this.floorShadow.graphics;
		gr.beginFill("#000050").drawCircle(0, 0, radius);

		floor.addChild(this.floorShadow);
		space.addChild(this.body);
	}

	Ball.prototype.contact = function(contactX, contactY, priority, force)
	{
		var tx = contactX - this.x;
		var ty = contactY - this.y;

		var angle = Math.atan2(ty, tx);
		var length = Math.sqrt(tx * tx + ty * ty);

		this.vx -= (1.0 - length / this.radius) * force * Math.cos(angle);
		this.vy -= (1.0 - length / this.radius) * force * Math.sin(angle);

		// 変形のための接触点は1つしか使用できない。床を優先させる
		if(this.priority < priority)
		{
			this.priority = priority;
			this.cx = tx;
			this.cy = ty;
		}
	}

	// 元のコードのupdateに相当
	Ball.prototype.update = function()
	{
		this.cx = 0;
		this.cy = 0;
		this.priority = 0;

		this.x += this.vx;
		this.y += this.vy;

		// 画面左端に接触している場合
		if(this.x - this.radius < 0)
		{
			this.contact(0, this.y, 1, 0.125);
		}
		// 画面右端に接触している場合
		if(this.x + this.radius > SCREEN_WIDTH)
		{
			this.contact(SCREEN_WIDTH, this.y, 1, 0.125);
		}
		// 床に接触している場合
		if(this.y + this.radius > FLOOR_Y)
		{
			this.contact(this.x, FLOOR_Y, 3, 0.125);
		}
		// 床に接触していなければ
		else
		{
			this.vy += 0.005;
		}

		// すごい勢いで壁にぶつかったときは即座に反射
		if(this.x < 0 || this.x > SCREEN_WIDTH)
		{
			this.vx *= -1;
		}
		if(this.y > FLOOR_Y)
		{
			this.vy *= -1;
		}

		// 画面外のボールをこっそり減速して、全体の動きを永続化
		if(this.y < -this.radius)
		{
			this.vx *= 0.9996;
			this.vy *= 0.9996;
		}
	}

	// 他のボールとの衝突を検出する関数
	Ball.prototype.hitTest = function(ball)
	{
		var dx = ball.x - this.x;
		var dy = ball.y - this.y;
		var distanceSquared = dx * dx + dy * dy;
		var contactDistance = this.radius + ball.radius;
		if(distanceSquared < contactDistance * contactDistance)
		{
			var tx = linearTransform(this.radius, 0, contactDistance, this.x, ball.x);
			var ty = linearTransform(this.radius, 0, contactDistance, this.y, ball.y);
			this.contact(tx, ty, 2, 0.2);
			ball.contact(tx, ty, 2, 0.2);
		}
	}

	Ball.prototype.render = function()
	{
		this.matrix.identity();
		if(this.priority > 0)
		{
			var angle = Math.atan2(this.cy, this.cx);
			var length = Math.sqrt(this.cx * this.cx + this.cy * this.cy);

			// 縦方向は適当
			this.matrix.scale(length / this.radius, 1 + (1 - length / this.radius));
			this.matrix.rotate(angle);
		}
		this.matrix.translate(this.x, this.y);
		this.matrix.decompose(this.body);
		
		var height = FLOOR_Y - (this.y + this.radius);
		this.floorShadow.x = this.x;
		this.floorShadow.y = FLOOR_Y + height * 0.3;
		this.floorShadow.alpha = linearTransform(height, 0, 300, 0.3, 0);

		var scale = 1.2 + height * 0.005;
		this.floorShadow.width = this.radius * 2 * scale;
		this.floorShadow.height = this.radius * 2 * scale * 0.2;
		this.floorShadow.scaleX = scale;
		this.floorShadow.scaleY = scale * 0.2;

		/*
		var blur = linearTransform(height, 0, 200, 3, 25);
		var blurFilter = new createjs.BlurFilter(blur, blur / 3, 1);
		this.floorShadow.filters = [blurFilter];
		var bounds = blurFilter.getBounds();
		this.floorShadow.cache(
			-this.floorShadow.width * 2 + bounds.x,
			-this.floorShadow.height * 2 + bounds.y,
			this.floorShadow.width * 4 + bounds.width,
			this.floorShadow.height * 4 + bounds.height);
		*/
	}
	
	// createJSのライブラリとして設定
	createjs.Ball = Ball;
}(window));

// Ballクラスを格納するための配列
var balls = new Array();

// 元のコードのinitializeに相当
function init()
{
	// HTMLのcanvasのidを指定して、stageに代入
	// これでASのstage感覚で扱えるようになる
	stage = new createjs.Stage(&#39;canvas&#39;);

	// canvasの大きさを取得
	SCREEN_WIDTH = stage.canvas.width;
	SCREEN_HEIGHT = stage.canvas.height;
	FLOOR_Y = 400;

	balls.push(new createjs.Ball(60, 0x000000));
	balls.push(new createjs.Ball(45, 0x4040FF));
	balls.push(new createjs.Ball(30, 0x20C040));
	balls.push(new createjs.Ball(30, 0xA0A0B0));
	balls.push(new createjs.Ball(20, 0xC08080));
	balls.push(new createjs.Ball(20, 0x706050));
	balls.push(new createjs.Ball(20, 0xF0A090));

	// 床の描画
	var background = new createjs.Shape();
	var g = background.graphics;
	g.beginLinearGradientFill(
		["#FFFFF8", "#E0E0E0", "#D0D0D0", "#FFFFF8"],
		[0, 150/255, 168/255, 170/255, 1.0],
		0, 0, 0, SCREEN_HEIGHT
		);
	g.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	g.endFill();
	stage.addChild(background);

	stage.addChild(space);
	stage.addChild(floor);

	// 更新間隔を60FPSに設定
	createjs.Ticker.setFPS(60);
	
	// window、つまりstageに onEnterFrame()を
	// 設定するのと同等と思ってもらって構わない
	createjs.Ticker.addEventListener("tick", tick);
}

// 元のコードのenterFrameHandlerに相当
function tick()
{
	// ボール同士の衝突判定を行う
	for(var i = 0; i < 4; ++i)
	{
		for(var j = 0; j < balls.length; ++j)
		{
			balls[j].update();
		}
		for(var n = 0; n < balls.length; ++n)
		{
			for(var m = n + 1; m < balls.length; ++m)
			{
				balls[n].hitTest(balls[m]);
			}
		}
	}

	for(var i = 0, n = balls.length; i < n; ++i)
	{
		balls[i].render();
	}

	stage.update();
}

// 範囲内でランダムな値を生成
function randomRange(min, max)
{
	return Math.random() * (max - min) + min;
}


function linearTransform(n, s0, s1, d0, d1)
{
	return (d0 + (n - s0) * (d1 - d0) / (s1 - s0));
}