エンドロールムービーをprocessingで作る #1 【連番画像作成・アニメーション】

**Processing**と**ffmpeg**を用いて簡単な結婚式風エンドロールムービーを作成しました。
**Processingで連番の画像を作成**し、**ffmpegによりその連番画像を動画に変換**しています。
動画作成方法を数回に分けて紹介していきたいと思います。

**2021/10/3**
[本プログラム紹介の#2](https://qiita.com/takenoko300/items/3596d1c3b5ee03f9b437)を作成いたしました。そちらでは**文字スクロール・画像表示**を紹介しております。

https://qiita.com/takenoko300/items/3596d1c3b5ee03f9b437

# 動作環境
Windows 10
Processing 3.5.3

# 今回作成した動画
![out_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/930a8b16-204e-9ec0-f6a3-2c5ba66445f5.gif)

# 動画の作成方法
**Processing**でのプログラミングと、**ffmpeg**での動画作成に分けて説明を行います。

## Processingプログラミング内容説明
今回の動画を作成するにあたって使用した関数など、プログラミング内容を説明していきます。

### 連番画像の作成
連番画像の作成は**saveFrame()**という関数を使用して行います。試しに以下の文を実行するとframesフォルダが作成され、その中に00001、00002、00003…　と、saveFrame()引数の#に対応した桁数の連番画像が作成されます。
saveFrame()実行中は画像が際限なく作成されて動きがカクカクするので、動作確認時などはコメントアウトしておくことをお勧めします。

```java
int FR = 50;  //1秒間あたりのフレーム数(fps)
int cnt = 0;

void setup() {
  size(1600, 900);
  frameRate(FR);
}

void draw() {
  strokeWeight(3.0);
  strokeCap(ROUND);
  line(0,0, cnt, cnt);
  cnt++;
  if(cnt <= 200){
    saveFrame( "frames/#####.png" ); // framesフォルダに作成した画像を保存
  }
}
```

### フォントの変更
フォントの変更は**loadFont()**という関数を使用します。ただし、この関数を使用する前にprocessingで表示可能なフォーマットにフォントを変換する必要があります。
まずは、「ツール」→「フォント作成...」を選択します。
![loadfont1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/3518db43-1d43-eee3-4f73-731c198af81b.png)
ポップアップウィンドウが表示されるので、使用したいフォントを選択して「OK」をクリックします。
![loadfont2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/966e859c-ac79-56f7-d8a9-dd7128903632.png)
以上でフォントを使用する準備が完了です。こちらも必要なフォントをインストールして、試しに以下の文を実行するとフォントの異なる文字列が表示されます。

```java
String str = "Hello world";

void setup() {
  size(1600, 900);
}

void draw() {
  //テキスト表示用
  PFont font;
  float strX;
  float strY;
  float strSize;
  int strAlign;

  font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");  // 任意のフォントを読み込み
  textFont(font);
  strX = width/2;     //文字列x座標
  strY = height/2;    //文字列y座標
  strSize = 48;       //文字列サイズ設定
  strAlign = CENTER;  //文字列中央寄せ
  
  textSize(strSize);     //文字列のサイズ
  textAlign(strAlign);   //文字列配置
  text(str, strX, strY); //文字列描画
  
  font = loadFont("Dialog.bolditalic-48.vlw");  // 任意のフォントを読み込み
  textFont(font);
  strX = width/2 + 100;
  strY = height/2 + 100;
  strSize = 60;
  strAlign = CENTER;  //文字列中央寄せ
  
  textSize(strSize);    //文字列のサイズ
  textAlign(strAlign);  //文字列配置
  text(str, strX, strY); 
}
```
![loadfont3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/eec7e6d1-83a9-b019-e5eb-dfb9ce46a7ca.png)

### 図形の透過
透過を行うことでアニメーションに動きを付けます。**fill()**を使用すれば図形や文字の塗りつぶし色の指定に加えて、透過の設定も行えます。詳細は以下の通りです。
fill( int colorVal , float alpha ) 
colorVal:色情報
alpha ： 透明度指定　0(透明) – 255(不透明) 

ここでは、alphaの値を時間によって変化させていくことでフェードイン・フェードアウトを行っています。

```java
int FR = 50;  //1秒間あたりのフレーム数(fps)
int cnt = 0;
String str = "Hello world";
float startTime = 5 * FR;     //文字列の表示時間(sec)
float endTime = 8 * FR;       //終了時間(sec)
int addVal = -1;              //減衰値
float cntAlpha;               //透過時間計算用
float cntAlphaMax = 3.0 * FR; //アニメーション時間(sec)

void setup() {
  size(1600, 900);
  frameRate(FR);
}

void draw() {
  //テキスト表示用
  PFont font;
  float strX;
  float strY;
  float strSize;
  int strAlign;
  
  background(0, 0, 0);  // 背景色を黒色に設定
  
  if((float)cnt <= (float)cntAlphaMax){
    addVal = 1;    //フェードイン
  }else if((float)cnt >= (float)(startTime - cntAlphaMax)){
    addVal = -1;    //フェードアウト
  }
  
  cntAlpha += addVal;
  if( cntAlpha < 0 ){
    cntAlpha = 0;
  } else if( cntAlpha > cntAlphaMax ){
    cntAlpha = cntAlphaMax;
  }  
  int alpha = (int)map(cntAlpha,0,cntAlphaMax,0,255);  //透過率
  fill(255, alpha);

  font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");  // 任意のフォントを読み込み
  textFont(font);
  strX = width/2;
  strY = height/2;
  strSize = 48;
  strAlign = CENTER;  //文字列中央寄せ
  
  textSize(strSize);    //文字列のサイズ
  textAlign(strAlign);  //文字列配置
  text(str, strX, strY); 
  
  font = loadFont("Dialog.bolditalic-48.vlw");  // 任意のフォントを読み込み
  textFont(font);
  strX = width/2 + 100;
  strY = height/2 + 100;
  strSize = 60;
  strAlign = CENTER;  //文字列中央寄せ
  
  textSize(strSize);    //文字列のサイズ
  textAlign(strAlign);  //文字列配置
  text(str, strX, strY); 
  
  if(cnt <= endTime){
    saveFrame( "frames/#####.png" );
  }
  
  cnt++;
}
```

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/9a3b9804-f544-7f09-16a0-01d98ba8c7be.gif)


## プログラミング全文

```java
//--------------グローバル変数--------------//
int cnt = 0;
int flag = 1;
int flagMax = 2;
PFont fontDefault;  //デフォルトのフォント設定用
int FR = 50;  //1秒間あたりのフレーム数(fps)

//テキストアニメーション用
float openingStartTime = 5 * FR;     //オープニングの表示時間(sec)
float openingStrTime = 8 * FR;     //オープニングメッセージの表示時間(sec)
int addVal = -1;              //減衰値
float cntAlpha;               //透過時間計算用
float cntAlphaMax = 1.0 * FR; //アニメーション時間(sec)

//ラインアニメーション用
float lineLen = 0;     //線の長さ
int addValLine = -1;   //減衰値
float cntLine;         //線描画時間計算用
int lineMin = 100 ;    //スタートポジション
float lineMax = 1600 - lineMin;  //エンドポジション
float cntLineMax = 1.0 * FR;     //アニメーション時間(sec)

void setup() {
  size(1600, 900);
  fontDefault = createFont("Serif.bold", 50);
  textFont(fontDefault);
  frameRate(FR);
}

void draw() {
  //テキスト表示用
  PFont font;
  String str;
  float strX;
  float strY;
  float strSize;
  int strAlign;
  
  //ライン表示用
  float lineX1;
  float lineY1;
  float lineX2;
  float lineY2;

  int alphaSub;
  float lineLenSub;
  
  background(0, 0, 0);
  
  switch(flag){
    case 1:    // オープニング画面
//--------------文字のアニメーション表示用--------------//
      if((float)cnt <= (float)cntAlphaMax){
        addVal = 1;    //フェードイン
      }else if((float)cnt >= (float)(openingStartTime - cntAlphaMax)){
        addVal = -1;    //フェードアウト
      }

//--------------ラインアニメーション表示用--------------//
      if(cnt <= cntLineMax){
        addValLine = 1;    //フェードイン
      }else if(cnt >= openingStartTime - cntLineMax){
        addValLine = -1;    //フェードアウト
      }    
    
//--------------オープニングタイトル--------------//
//--------------ラインアニメーション--------------//
      //上側の線
      lineLenSub = lineAnimation();
      stroke(255);
      lineX1 = lineMin;
      lineY1 = height/2 - 90;
      lineX2 = lineLenSub;
      lineY2 = lineY1;
      lineDraw(lineX1, lineY1, lineX2, lineY2);

      //下側の線
      lineLenSub = lineAnimation();
      stroke(255);
      lineX1 = lineMax - lineLenSub + 100;
      lineY1 = height/2 + 90;
      lineX2 = lineMax;
      lineY2 = lineY1;
      lineDraw(lineX1, lineY1, lineX2, lineY2);

      //線を隠すために透明な長方形を線の上に描画
      fill(0);
      noStroke();
      rect(width/2 - 300, height/2 - 100, 600, 20);


//--------------１行目--------------//
      font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");
      textFont(font);
      str = "26  September  2021";
      strSize = 48;
      strX = width/2;
      strY = height/2 - 80;
      strAlign = CENTER;

      alphaSub = strAnimation();
      fill(255, alphaSub);
      textDraw(str, strSize, strAlign, strX, strY);

//--------------2行目--------------//
      font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");
      textFont(font);
      str = "Thanks For Guests";
      strSize = 100;
      strX = width/2;
      strY = height/2 + 30;
      strAlign = CENTER;      
      textDraw(str, strSize, strAlign, strX, strY);
      
      //表示時間計算用
      if(cnt >= openingStartTime){
         flag = 2;
         cnt = 0;
      }
      break;
  }
  cnt ++;
  
  if(flag != flagMax){
    saveFrame( "frames/#####.png" );
  }
}

private float lineAnimation(){
  //cntLineから線の長さを計算する
  cntLine += addValLine;
  if( cntLine < 0 ){
    cntLine = 0;
  } else if( cntLine > cntLineMax ){
    cntLine = cntLineMax;
  }  
  lineLen = (float)map(cntLine,0,cntLineMax,lineMin,lineMax);
  return lineLen;
}

private void lineDraw(float x1, float y1, float x2, float y2){
  strokeWeight(3.0);
  strokeCap(ROUND);
  line(x1, y1, x2, y2);  
}

private int strAnimation(){
  //cntAlphaからアルファ値を計算する
  cntAlpha += addVal;
  if( cntAlpha < 0 ){
    cntAlpha = 0;
  } else if( cntAlpha > cntAlphaMax ){
    cntAlpha = cntAlphaMax;
  }  
  int alpha = (int)map(cntAlpha,0,cntAlphaMax,0,255);  //透過率
  return alpha;
}

private void textDraw(String str, float s, int a, float x, float y){
  textSize(s);    //文字列のサイズ
  textAlign(a);  //文字列中央寄せ
  text(str, x, y); 
}
```

## ffmpegでの動画作成
ffmpegはコマンドラインでアプリケーションです。まずは以下を参考にインストールして、コマンドラインで使用できるように設定を行います。

https://qiita.com/SatoshiGachiFujimoto/items/0da89db99d8a5dae3e9c

後は以下を参考にして連番画像から動画を出力することができれば成功です。

https://qiita.com/livlea/items/a94df4667c0eb37d859f

```
ffmpeg -framerate 50 -i %05d.png -vcodec libx264 -pix_fmt yuv420p -r 50 out.mp4
```

# 参考
・saveFrame()
http://www.musashinodenpa.com/p5/index.php?pos=1373

・frameRate()
http://www.musashinodenpa.com/p5/index.php?pos=162

・loadFont()
https://r-dimension.xsrv.jp/classes_j/system_font/

・fill()
http://mslabo.sakura.ne.jp/WordPress/make/processing%E3%80%80%E9%80%86%E5%BC%95%E3%81%8D%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/%E5%9B%B3%E5%BD%A2%E3%82%84%E6%96%87%E5%AD%97%E3%82%92%E9%80%8F%E6%98%8E%E3%81%AB%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF/

・ffmpegインストール
https://qiita.com/SatoshiGachiFujimoto/items/0da89db99d8a5dae3e9c

・ffmpeg使用方法
https://qiita.com/livlea/items/a94df4667c0eb37d859f