PHP CLIでロードアニメーションを実現する


# 結論

出来る。
しかもかなりこった事できる。

![load.gif](https://qiita-image-store.s3.amazonaws.com/0/81573/c98f8363-8d49-7ede-9b7c-e5c1d11f8bac.gif)


# 環境

PHP 7.0.2 + bash on CentOS 7
PHP 7.0.2 + Git Bash + ConEmu on Windows 10 Pro
PHP 7.0.2 + Windows PowerShell + ConEmu on Windows 10 Pro
PHP 7.0.2 + cmd.exe + ConEmu on Windows 10 Pro
※色表示が出来る手段が無ければ、Git Bash、Windows PowerShellともに動作不能。
※但し、ConEmu経由で起動させればcmd.exeすら動作する。ConEmu最高かよ。
=>[ConEmu プロジェクト日本語トップページ - OSDN](https://osdn.jp/projects/conemu/)

# でもgifファイルをそのまま動かせるわけではないのでしょう？

んなもんラインアレイに色並べて一つずつずらせばいいんですよ。

凄い単純化するとこう。

```
[10000]
[01000]
[00100]
[00010]
[00001]
```
これを1行で繰り返せば"1"が右に動いて行っているように"見える"

ね？簡単でしょう？

# では色は？

[前の投稿に頂いたコメント](http://qiita.com/ProjectICKX/items/05e343fc762a93cda94a#comment-7bde5f7b043fae7bdc77)のリンク先に色々あったのでそのまま利用。

# 実装

最少構成

```php
$seed = array_merge(range(16, 21), range(21, 16));

$length = 10;

for ($i = 0;$i < 100;$i++) {
	for ($n = 0;$n < $length;$n++) {
		//配列の内部ポインタだけ動かしているので大変ローコスト
		echo sprintf("\e[48;5;%dm \e[0m", ($num = next($seed)) === false ? reset($seed) : $num);
	}
	usleep('100000');
	echo "\r";
}
echo "\r";
echo PHP_EOL;
```

関数化したもの

```php:test.php
<?php
/**
 * usage:
*
* コマンドラインで次のコマンドを入力し、Enterを押下してください。
* （php binaryへのパスは通っているものとします）
* php test.php
*
* その後"please input count:"と表示されるので、任意の数字を入力し、Enterを押下してください。
*/
//======================================================
//初期設定
//======================================================
//おやくそく
date_default_timezone_set('Asia/Tokyo');

//繰り返し中の待ち時間 単位はマイクロ秒 50000の場合は50ミリ秒
define('WAIT_TIME', 50000);

//======================================================
//実処理
//======================================================
//ここで繰り返し用数値を取得
echo 'please input count:';
$all_count = trim(fgets(STDIN));
echo PHP_EOL;

//プログレスバー用の関数生成
$progress = create_progress($all_count);

//繰り返し部
for ($i = 1;$i <= $all_count;$i++) {
	//プログレスバーの表示
	$progress();

	//ちょっとだけスリープする。
	usleep(WAIT_TIME);
}

//最後に改行いれないと見づらくなる。
echo PHP_EOL;

//======================================================
//以下関数
//======================================================
/**
 * プログレスバーを生成する関数を生成します。
 *
 * usage:
 * $all_count = count($array);
 * $progress create_progress($all_count);
 * foreach ($array as $row) {
 *     // $rowを使った何らかの処理
 *     $progress();
 * }
 *
 * 引数無しで$progress();を利用し、進捗が100%まで到達した場合、内部で初期化が入る為、そのまま再利用できます。
 *
 * @param	int		$all_count		対象とする処理の全数
 * @param	array	$progress_chars	プログレスバーでバーとして利用する文字設定
 *	[
 *		'finished'		=> string 終了済みを示す文字 キー名を省略した場合、0番目の要素を使う
 *		'current'		=> string 現在位置を示す文字 キー名を省略した場合、1番目の要素を使う
 *		'unfinished'	=> string 完了済みを示す文字 キー名を省略した場合、2番目の要素を使う
 *	]
 * @param	string	$format			プログレスバー表示フォーマット sprintf互換
 *	位置指定子に対する内容
 *		1$：現在の進捗率
 *		2$：プログレスバーの終了分
 *		3$：プログレスバーの現在位置の文字
 *		4$：プログレスバーの未了分
 *		5$：予想残り時間
 *		6$：全数に対する作業完了数
 * @return	callable	プログレスバーを表示する関数
 *	@param			int	$current	対象とする処理の現在位置 開始値は1 省略すると現在の値に+1した値で実行する
 *	@throws		\Exception		$currentが1未満の数字を与えると例外が発生する
 */
function create_progress ($all_count, $progress_chars = [], $format = null) {
	//======================================================
	//関数作成前の初期化
	//======================================================
	//プログレスバー文字の確定
	$finished_str	= isset($progress_chars['finished']) ? $progress_chars['finished'] : (isset($progress_chars[0]) ? $progress_chars[0] : '|');
	$current_str	= isset($progress_chars['current']) ? $progress_chars['current'] : (isset($progress_chars[1]) ? $progress_chars[1] : '|');
	$unfinished_str	= isset($progress_chars['unfinished']) ? $progress_chars['unfinished'] : (isset($progress_chars[2]) ? $progress_chars[2] : ' ');

	//プログレスバーフォーマットの確定
	if ($format === null) {
		$format = sprintf(' %% 3s%%%% [%%s%%s%%s] ETA %%s %% %ss/%s', strlen($all_count), $all_count);
	}

	//ロードアニメーション用色指定
	$colors = array_merge(range(16, 21), range(21, 16));

	//======================================================
	//関数構築
	//======================================================
	return function ($current = null) use ($all_count, $finished_str, $current_str, $unfinished_str, $format, $colors) {
		//======================================================
		//初期処理
		//======================================================
		//プログレスバー実行開始時点の時間保持用変数
		static $start_ts;

		//直前に表示されたプログレスバーの文字列長
		static $before_width;

		//最後に実行した位置
		static $position;

		//現在しようしているロードアニメーションカラー
		static $loader_color;

		//現在のspace幅
		static $color_bar_width;

		//現在の時間を取得
		$current_ts = microtime(true);

		//======================================================
		//検証
		//======================================================
		if ($current !== null && $current < 1) {
			throw new \Exception(sprintf('現在位置は1以上の数値のみ使用できます。$current:%s', $current));
		}

		//======================================================
		//クロージャ―変数の初期化
		//======================================================
		if ($current === null) {
			if ($position === null) {
				$position = 0;
			}
			$position++;
		} else {
			$position = $current;
		}

		//プログレスバー初期化処理
		if ($position === 1) {
			$start_ts = $current_ts;
			$loader_color = $colors;
			end($loader_color);
		}

		//======================================================
		//実処理
		//======================================================
		//プログレスバー実行開始からの経過時間
		$elapsed_ts = ($current_ts - $start_ts) / $position * ($all_count - $position);

		//予想終了時間の算出
		$eta = sprintf('%02.2s:%02s:%02s.%0-3.3s', $elapsed_ts / 60 / 60 % 60, $elapsed_ts / 60 % 60, $elapsed_ts % 60, round(($elapsed_ts - floor($elapsed_ts)) * 1000));

		//進捗状況の算出
		$percent = $position / $all_count * 100;
		$progress = round($percent / 2);

		//プログレスバーの構築
		$progress_bar = sprintf($format, round($percent), str_repeat($finished_str, $progress), $current_str, str_repeat($unfinished_str, 50 - $progress), $eta, $position);

		//直前に表示したプログレスバーの文字列長が現在よりも長い場合、消しこみ処理を追加する。
		$current_width = mb_strwidth($progress_bar);
		$sol = '';
		if ($before_width > $current_width) {
			$diff_width = $before_width - $current_width;
			$sol = sprintf('%s%s', str_repeat(' ', $diff_width), str_repeat("\x08", $diff_width));
		}
		$before_width = $current_width;

		//ロードアニメーションの構築
		$loeader_bar = [];
		for ($i = 0;$i < 80;$i++) {
			$loeader_bar[] = sprintf("\e[48;5;%dm \e[0m", ($num = next($loader_color)) === false ? reset($loader_color) : $num);
		}
		$loeader_bar = implode('', $loeader_bar);

		//現在位置が$all_countと同一になった場合、初期化して終わる。
		if ($all_count == $position) {
			$start_ts		= null;
			$before_width	= null;
			$position		= null;
			end($loader_color);
			$eol			= '';
		} else {
			//末尾につける "\r" が全ての答えだった。"\n"に変えると良く判る。
			$eol			= "\r";
		}

		//出力
		echo "\e[1A";
		printf('%s%s%s', $sol, $loeader_bar, $eol);
		echo PHP_EOL;
		printf('%s%s%s', $sol, $progress_bar, $eol);
	};
}
```