PHPでアニメーション画像を生成する

PHPにはGDモジュールという画像処理系のモジュールが用意されています。これが入っていると画像処理•図形描画系の関数が使用する事ができ、基本的な機能は一通り用意されています。
例えばさくらのレンタルサーバーやロリポップでもこれらの関数を標準で使えたりするので、PHPでちょっとした画像処理や図形描画をする敷居は結構低いんじゃないかな、と思ってます。
軽くググってみた感じ、PHPでの図形描画やアニメーションについて紹介をしている記事があまり見つからなかりませんでした。恐らくあまり需要がないというのが理由なのだと思いますが、面白そうなのでサンプルを作りながら軽くPHPでのアニメーション表現について紹介してみようと思います。

前置き

GDの関数は結構癖があります。単純にリサイズしたりする程度なら簡単に実装でき問題ありませんが、少し複雑な事をしようとすると、その癖を知っておく必要があり面倒です。
なので、今回は生でGD系の関数を使うのでは無く、DmImageというラッパーライブラリを使用する事にします。

http://demouth.hatenablog.com/entry/2012/11/14/075806
https://github.com/demouth/DmImage/
http://www.moongift.jp/2012/11/20121125/

1. ProcessingのソースコードPHPに移植

まずはメジャーなアニメーション表現のソースコードPHPに移植しながら、説明をしていこうと思います。

ビジュアル表現でメジャーなのはProcessingだと思います。ProcessingはJavaをベースにしつつビジュアル表現に特化したプログラミング環境となっており、開発環境の準備が手軽で、さらにサンプルコードもデフォルトで数多く用意されている為とっつきやすく人気が高いようです。
このサンプルコードの中からPHPへ移植しやすそうなサンプルをピックアップしてみました。
Treeというサンプルコードです。
http://www.processing.org/learning/topics/tree.html

ソースコードは下記の通りです

float theta;   

void setup() {
  size(640, 360);
}

void draw() {
  background(0);
  frameRate(30);
  stroke(255);
  // Let's pick an angle 0 to 90 degrees based on the mouse position
  float a = (mouseX / (float) width) * 90f;
  // Convert it to radians
  theta = radians(a);
  // Start the tree from the bottom of the screen
  translate(width/2,height);
  // Draw a line 120 pixels
  line(0,0,0,-120);
  // Move to the end of that line
  translate(0,-120);
  // Start the recursive branching!
  branch(120);

}

void branch(float h) {
  // Each branch will be 2/3rds the size of the previous one
  h *= 0.66;
  
  // All recursive functions must have an exit condition!!!!
  // Here, ours is when the length of the branch is 2 pixels or less
  if (h > 2) {
    pushMatrix();    // Save the current state of transformation (i.e. where are we now)
    rotate(theta);   // Rotate by theta
    line(0, 0, 0, -h);  // Draw the branch
    translate(0, -h); // Move to the end of the branch
    branch(h);       // Ok, now call myself to draw two new branches!!
    popMatrix();     // Whenever we get back here, we "pop" in order to restore the previous matrix state
    
    // Repeat the same thing, only branch off to the "left" this time!
    pushMatrix();
    rotate(-theta);
    line(0, 0, 0, -h);
    translate(0, -h);
    branch(h);
    popMatrix();
  }
}


tree_processing - YouTube

まずProcessingの言語説明を軽くしておきます。Processingはsetup()関数を宣言しておくと一番最初に実行されます。この関数の中にはウィンドウサイズの初期化やフレームレートの指定などを書いたりします。
そしてdraw()関数が1フレーム毎に呼ばれるので、ここに描画処理を書いておきます。

次にこのProcessingのサンプルコードのdraw()関数の描画処理について説明をします。
このサンプルでは、最初にマウス座標を取得し、このマウス座標を種として、二本の枝を再帰的に描画していきます。再帰的に描画する際、二本の枝を段々短かくなるように描画していき、ある程度の長さ未満になったら1フレーム分の描画を終了します。
そして1フレーム分の描画が終わると、一定時間後に再度draw()関数が呼び出され、上記の処理を繰り返します。それによりマウスの座標によって枝の角度が変わるアニメーションになって見えます。

これをPHPに移植しますが、いきなりアニメーションを実装するのは難しいので一旦静止画を生成する処理として移植する事にします。また、サーバー側で全ての処理を行うため、マウス座標を取得せずランダム値を使い、一回分の描画処理の後にgif画像として出力する事にします。
なお、Processingにデフォルトで用意されている関数の多くは、PHPでは用意されていないので、なるべく近いコードになるように移植します。
※特にpushMatrix()やグローバル変数の使い方が違っています。

移植結果の画像と、ソースコードは下記になります。

<?php
define('WIDTH', 640);
define('HEIGHT', 360);

$theta = rand(1,20)*0.1;
$image = new Dm_Image(WIDTH,HEIGHT,0xFF000000);
draw();
$image->display();

function draw()
{
	global $image;
	
	$image->graphics
		->lineStyle(1,0xFFFFFFFF)
		->moveTo(WIDTH/2, HEIGHT)
		->lineTo(WIDTH/2, HEIGHT-120);
	$h = 120;
	branch($h,-M_PI/2,WIDTH/2,HEIGHT-120);
}
function branch($h,$rotate,$x,$y)
{
	global $image,$theta;
	if($h<2)return;
	
	$h *= 0.66;
	
	$movedX = cos($rotate+$theta)*$h + $x;
	$movedY = sin($rotate+$theta)*$h + $y;
	$image->graphics
		->moveTo($x, $y)
		->lineTo($movedX, $movedY);
	branch($h,$rotate+$theta,$movedX,$movedY);
	
	$movedX = cos($rotate-$theta)*$h + $x;
	$movedY = sin($rotate-$theta)*$h + $y;
	$image->graphics
		->moveTo($x, $y)
		->lineTo($movedX, $movedY);
	branch($h,$rotate-$theta,$movedX,$movedY);
}

global キーワードを使ったり引数が多かったり結構無理やりです。。

f:id:y_d:20130416071342p:plain

PHPから出力された画像は上記の通りで、結構再現度高い気がします。

アニメーションにしてみる

静止画を出力する事に成功しましたので、今度はアニメーションさせてみます。
先程作成したPHPのコードを書き換え、複数の画像ファイルを出力させてしまえば、あとは複数の画像ファイルをアニメーションにする方法はいくつかあります。例えばImageMagickを使ったり、インストール型のフリーソフトやWebサービスなどなどいろいろありますが、せっかくなのでこれもPHPでやっちゃいます。
アニメーションgifを作るライブラリがあるので(こちらを参照)、これを少し使いやすくラップしたクラスを作っておきます。

<?php
/**
 * @example
 * <code>
 * $merge = new SimpleGifMerge('/path/to/dir/');
 * $merge->merge(); //ディレクトリ内のすべての画像を結合して表示
 * $merge->clear(); //ディレクトリ内のファイルをすべて削除
 * </code>
 * @see http://d.hatena.ne.jp/shimooka/20060914/1158209427
 * @see http://www.phpkode.com/source/s/gif-images-into-animated-gif-with-native-php-class/gif-images-into-animated-gif-with-native-php-class/GifMerge.class.php
 */
require('GifMerge.class.php');

class SimpleGifMerge{
	public $path;
	public function __construct($path){
		$this->path = $path.'/';
	}
	public function merge(){
		if ($handle = opendir($this->path)) {
			$c = 0;
			$files = array();
			while (false !== ($file = readdir($handle))) {
				if(!is_file($this->path.$file))continue; 
				$files[] = realpath($this->path.$file);
				$c++;
			}
			sort($files);
			closedir($handle);
		}
		$d = array_fill(0, $c, 5);
		$x = array_fill(0, $c, 0);
		$y = array_fill(0, $c, 0);
		$anim = new GifMerge($files, 255, 255, 255, 0, $d, $x, $y, 'C_FILE');
		header('Content-type: image/gif');
		echo $anim->getAnimation();
	}
	public function clear(){
		if ($handle = opendir($this->path)) {
			while (false !== ($file = readdir($handle))) {
				if(is_file($this->path.$file)) unlink($this->path.$file);
			}
		}
	}
}

そして前述の移植版プログラムを、少しずつ図形を変化させた画像を100枚ディレクトリに書き出すプログラムに書き換え、最後にgif画像としてマージさせてみます。

<?php
define('DIR_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR.'img'.DIRECTORY_SEPARATOR);
define('WIDTH', 640);
define('HEIGHT', 360);

$merge = new SimpleGifMerge(DIR_PATH);
$merge->clear();
$l = 100;
for ($i=0; $i < $l; $i++) { 
	$theta = sin($i/$l*pi())*2;
	$image = new Dm_Image(WIDTH,HEIGHT,0xFF000000);
	draw();
	$count = sprintf('%05d', $i);
	$image->saveTo(DIR_PATH.$count.'.gif','gif');
}
$merge->merge();

function draw()
{
	global $image;
	
	$image->graphics
		->lineStyle(1,0xFFFFFFFF)
		->moveTo(WIDTH/2, HEIGHT)
		->lineTo(WIDTH/2, HEIGHT-120);
	$h = 120;
	branch($h,-M_PI/2,WIDTH/2,HEIGHT-120);
}

function branch($h,$rotate,$x,$y)
{
	global $image,$theta;
	if($h<2)return;
	
	$h *= 0.66;
	
	$movedX = cos($rotate+$theta)*$h + $x;
	$movedY = sin($rotate+$theta)*$h + $y;
	$image->graphics
		->moveTo($x, $y)
		->lineTo($movedX, $movedY);
	branch($h,$rotate+$theta,$movedX,$movedY);
	
	$movedX = cos($rotate-$theta)*$h + $x;
	$movedY = sin($rotate-$theta)*$h + $y;
	$image->graphics
		->moveTo($x, $y)
		->lineTo($movedX, $movedY);
	branch($h,$rotate-$theta,$movedX,$movedY);
	
}

この実行結果は下記の通りです。
※100枚分の画像を生成するので何秒かかかります(環境によりますが)。

f:id:y_d:20130417012659g:plain

結構それっぽくなったと思います。
PHPでも気軽にアニメーション描画を実装出来る事が証明できたでしょうか。

2. JavaScriptソースコードPHPに移植

JavaScript(Canvas)で実装した映画マトリックスっぽい表現が、数週間前にGigazineで紹介され流行りました。
ソースコードが短かく簡単に移植できそうなので、これもPHPに移植してみます。

http://gigazine.net/news/20130321-matrix-javascript/


まず、移植の前にJavaScriptのコードにコメントを書いて解析してみます。

// WindowのサイズにCanvas要素をリサイズする。
var s = window.screen;
var width = q.width = s.width;
var height = q.height = s.height;

// 1が入った配列を256個用意する
// この配列が文字一つ一つを表す
var letters = Array(256).join(1).split('');

// 描画処理を書いたFunction
// Processingでいうdraw()関数と同じ
var draw = function () {
  // 塗りの色を指定する
  // 黒の半透明を指定している
  q.getContext('2d').fillStyle='rgba(0,0,0,.05)';
  // Canvasの左上から右下までの全面を塗りつぶす
  // これによって文字が段々消えていき、残像の様な表現になっている
  q.getContext('2d').fillRect(0,0,width,height);
  // 色を緑色に指定する
  q.getContext('2d').fillStyle='#0F0';
  // 上記で作った256の配列の一個一個に対してループする
  // 引数y_posが配列の中身で、y座標が入っている
  // 引数indexが配列の番号(index)を表す
  letters.map(function(y_pos, index){
    // 特定範囲の文字コードから1文字分の文字列を取得する
    // この範囲は漢字
    text = String.fromCharCode(3e4+Math.random()*33);
    // 配列番号の10倍をx座標とする
    x_pos = index * 10;
    // ここまでで取得したxy座標を元にCanvasに文字を描画する
    q.getContext('2d').fillText(text, x_pos, y_pos);
    // 758から10758までの乱数よりもy座標が大きければy座標を(配列の中身を)0に戻す
    // そうでなければy座標を+10する
    letters[index] = (y_pos > 758 + Math.random() * 1e4) ? 0 : y_pos + 10;
  });
};

// draw関数を33ミリ秒おきに呼び出す
setInterval(draw, 33);

上記の実装を、PHPで静止画を出力する様に移植してみました。

<?php
define('W', 640);
define('H', 360);
define('FONT_SIZE', 11);
define('FRAME', 200);

$STR = array('','','','','','','','','','','','','','');
$letters = array_fill(0,((int)W/FONT_SIZE),0);
$image = new Dm_Image(W,H,0xFF000000);
$graphics = $image->graphics;
$textGraphics = $image->textGraphics;
$graphics->fillStyle(0x22000000);
for($i=0;$i<FRAME;$i++) draw();
$image->display();

function draw()
{
	global $STR,$letters,$graphics,$textGraphics;
	
	$graphics->drawRect(0, 0, W, H);
	$l = count($letters);
	for($i=0;$i<$l;$i++){
		$str = $STR[rand(0,count($STR)-1)];
		$y = $letters[$i];
		$textGraphics
			->setColor(0xFF00FF00)
			->setFontSize(FONT_SIZE-3)
			->textTo($i*FONT_SIZE, $y, $str);
		$letters[$i] = ($y > H + rand(0,1e3)) ? 0 : $y+FONT_SIZE;
	}
}

f:id:y_d:20130416071649p:plain

JavaScript版と違う主な部分は、

  • PHPにはマルチバイトに対応した文字列を生成する関数がない為、決めうちの配列を用意している
  • 適当に決めたフレーム数まで到達したら画像出力する様になっている。
  • Windowサイズを取得せず、決めうちの値を指定している

などなどです。

そして、これをアニメーション化させるとこんな感じになります。

<?php
define('DIR_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR.'img'.DIRECTORY_SEPARATOR);
define('W', 640);
define('H', 360);
define('FONT_SIZE', 11);
define('FRAME', 150);

$merge = new SimpleGifMerge(DIR_PATH);
$merge->clear();

$STR = array('','','','','','','','','','','','','','');
$letters = array_fill(0,((int)W/FONT_SIZE),0);
$image = new Dm_Image(W,H,0xFF000000);
$graphics = $image->graphics;
$textGraphics = $image->textGraphics;
$graphics->fillStyle(0x22000000);
for($i=0;$i<FRAME;$i++) {
	draw();
	$count = sprintf('%05d', $i);
	$image->saveTo(DIR_PATH.$count.'.gif','gif');
}
$merge->merge();

function draw()
{
	global $STR,$letters,$graphics,$textGraphics;
	
	$graphics->drawRect(0, 0, W, H);
	$l = count($letters);
	for($i=0;$i<$l;$i++){
		$str = $STR[rand(0,count($STR)-1)];
		$y = $letters[$i];
		$textGraphics
			->setColor(0xFF00FF00)
			->setFontSize(FONT_SIZE-3)
			->textTo($i*FONT_SIZE, $y, $str);
		$letters[$i] = ($y > H + rand(0,1e3)) ? 0 : $y+FONT_SIZE;
	}
}

f:id:y_d:20130416071709g:plain

見た目はJavaScript版に大分近いのではないかと思います。

3. オリジナルアニメーション

ここまで移植をしてきましたので、最後にオリジナルのアニメーションを作ってみる事にします。
※今から実装するのはProcessingやFlashなどでたまに見かける表現ではありますが。

仕様はこんな感じです。

  • 複数のパーティクルがランダムな座標からランダムな速度で等速移動する
  • パーティクルどうしが近ければ線で結び、近ければ近い程太い線で結ぶ(ただし一定以上の太さにはならない)
  • パーティクルを中心に円を描画し、近くにパーティクルがあればある程大きな円を描画する(ただし一定以上の太さにはならない)

これをPHPで実装すると下記の様になりました。
まずは静止画バージョンから。

<?php
define('W', 640);
define('H', 360);
define('C', 200);
define('D', 50);

$image = new Dm_Image(W,H,0xFF000000);
$graphics = $image->graphics;
$graphics
	->lineStyle(0)
	->fillStyle(0x66FFFFFF);

$nodeList = array();
for ($i=0; $i < C; $i++) { 
	$nodeList[] = array(
		rand(-20,W+20),
		rand(-20,H+20),
		0
	);
}

for ($i=0; $i < C; $i++) {
	$node = &$nodeList[$i];
	
	for ($j=$i; $j < C; $j++) {
		if($i==$j)continue;
		$nNode = &$nodeList[$j];
		$x = $node[0] - $nNode[0];
		$y = $node[1] - $nNode[1];
		$diff = sqrt($x*$x+$y*$y);
		if($diff<D){
			$node[2] += 1;
			$nNode[2] += 1;
			$graphics
				->lineStyle(
					min(10*(D-$diff)/D,4),
					Dm_Color::argb((D-$diff)/D*0.6+0.2,255,255,255)->toInt()
				)
				->moveTo($node[0], $node[1])
				->lineTo($nNode[0], $nNode[1])
			;
		}
	}
}

$graphics->lineStyle(0,0x11FFFFFF);
for ($i=0; $i < C; $i++) {
	$node = $nodeList[$i];
	$graphics->drawCircle($node[0], $node[1], 2+$node[2]*$node[2]*0.3);
}

$image->display();

f:id:y_d:20130416071515p:plain

そしてアニメーションバージョンは下記の通り。

<?php

define('DIR_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR.'img'.DIRECTORY_SEPARATOR);
define('W', 640);
define('H', 360);
define('C', 200);
define('D', 50);
define('FRAME', 150);

$merge = new SimpleGifMerge(DIR_PATH);
$merge->clear();

$nodeList = array();
for ($i=0; $i < C; $i++) { 
	$nodeList[] = array(
		rand(-20,W+20),
		rand(-20,H+20),
		0,
		rand(-10,10)*0.5,
		rand(-10,10)*0.5
	);
}

for ($i=0; $i < FRAME; $i++) { 
	draw($nodeList, $i);
}

$merge->merge();

function draw(&$nodeList,$c){
	$image = new Dm_Image(W,H,0xFF000000);
	$graphics = $image->graphics;
	$graphics
		->lineStyle(0)
		->fillStyle(0x66FFFFFF);
	
	for ($i=0; $i < C; $i++) {
		$node = &$nodeList[$i];
		$node[2] = 0;
	}
	
	for ($i=0; $i < C; $i++) {
		$node = &$nodeList[$i];
		
		for ($j=$i; $j < C; $j++) {
			if($i==$j)continue;
			$nNode = &$nodeList[$j];
			$x = $node[0] - $nNode[0];
			$y = $node[1] - $nNode[1];
			$diff = sqrt($x*$x+$y*$y);
			if($diff<D){
				$node[2] += 1;
				$nNode[2] += 1;
				$graphics
					->lineStyle(
						min(10*(D-$diff)/D,4),
						Dm_Color::argb((D-$diff)/D*0.6+0.2,255,255,255)->toInt()
					)
					->moveTo($node[0], $node[1])
					->lineTo($nNode[0], $nNode[1])
				;
			}
		}
	}
	
	$graphics->lineStyle(0,0x11FFFFFF);
	for ($i=0; $i < C; $i++) {
		$node = $nodeList[$i];
		$graphics->drawCircle($node[0], $node[1], 2+$node[2]*$node[2]*0.2);
	}
	
	for ($i=0; $i < C; $i++) {
		$node = &$nodeList[$i];
		$node[0] += $node[3];
		$node[1] += $node[4];
	}
	
	$count = sprintf('%05d', $c);
	$image->saveTo(DIR_PATH.'img'.$count.'.gif','gif');
}

f:id:y_d:20130416071533g:plain

これがPHPで動いているとは、なんだか不思議な感じがします。

最後に

いかがでしたでしょうか。
PHPでもこんな事出来るなんて面白いですよね。
アニメーションの実装に興味があるPHPerはもちろん、PHPに興味があるASerやJSer方も試してみてもらうと面白いのではないかと思います。

なお詳しいソースはgithubにありますのでご興味ある方は参照ください。
https://github.com/demouth/DmImage/
http://demouth.github.io/DmImage/