エンドロールムービーをprocessingで作る #2 【文字スクロール・画像表示】

**Processing**と**ffmpeg**を用いた結婚式風エンドロールムービー作成方法紹介の**2回目**です。
今回は**文字のスクロール**と**画像表示**について紹介します。
1回目に紹介した**連番画像作成・アニメーション**については[こちらをご覧ください](https://qiita.com/takenoko300/items/538f5c3a3fb94758ba03)。

https://qiita.com/takenoko300/items/538f5c3a3fb94758ba03

# 今回作成した動画
![out3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/19af64dd-e7ad-fcdc-809a-148644b749b2.gif)

## プログラミング内容説明

### 文字のスクロール
文字のスクロールはループごとにY座標の位置を少しづつ変化させていくことで実現します。
そのため、ループごとにカウントアップされるグローバル変数（下記プログラムの cnt）を用意し、そのグローバル変数を利用して文字列のY座標を減算していきます。

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

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

float guestMoveStrSize = 50;  //文字のサイズ
float scrollSpeed = 3;        //文字が流れる速さ

//テキスト表示用
PFont font;
float strX;
float strY;
float strSize;
int strAlign;

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

void draw() {
  background(0, 0, 0);

  fill(255, 255);  // 文字色 白
  strSize = guestMoveStrSize;
  strX = width/2;
  strAlign = CENTER;
  textSize(strSize);    //文字列のサイズ
  textAlign(strAlign);  //文字列中央寄せ
    
  // カウントアップとともにリストの文字列Y座標が小さくなる（上方向に移動する）
  strY = height - (cnt * scrollSpeed);
    
  font = createFont("Serif.bold", 50);
  textFont(font);
  text(str, strX, strY);
  
  //スクロール終了判別用 最後の行のY座標が-100になれば終了
  if(strY <= -100){
    flag = 2;
  }
  
  if(flag != flagMax){
    saveFrame( "frames/#####.png" );
  }
  
  cnt++;  // ループごとにカウントアップ
}
```

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/7e32a088-dc05-8b2e-8dc8-7c800bab78d0.gif)

### 画像の表示
画像の表示は**loadImage()**関数で画像を読み込み、**image()**関数で指定座標に読み込んだ画像を表示することで行います。
複数画像を順番に表示させたいので、下準備として画像を表示させたい順番に img1.jpg、img2.jpg、img3.jpg・・・といったように**連番で保存**しておきます。後で紹介するサンプルプログラムの場合は以下のようなフォルダ構成となっています。

```
$ tree

├── sample.pde
├── img
|   ├── img1.jpg
|   ├── img2.jpg
|   ├── img3.jpg
|   ├── img4.jpg
|   └── img5.jpg
├── frames
└── data
```

画像の表示時にフェードイン、表示終了時にフェードアウトを行っています。
これは[1回目](https://qiita.com/takenoko300/items/538f5c3a3fb94758ba03#%E5%9B%B3%E5%BD%A2%E3%81%AE%E9%80%8F%E9%81%8E)でも紹介している、**透過率の設定**で行っています。画像よりも少し大きく背景色と同じ色の長方形を描画し、その長方形の透過率を変更することでフェードイン・フェードアウト動作を実現しています。

```java
//--------------グローバル変数--------------//
int FR = 50;  //1秒間あたりのフレーム数(fps)
int flag = 1;
int flagMax = 2;

//画像表示用
float imgTime = 3 * FR;  //画像表示時間
int imgNum = 1;     // 画像番号の最小値
int imgNumMax = 5;  // 画像番号の最大値
PImage img;
float imgW;
float imgH;
float imgX;
float imgY;
float cntImg = 0;
String strImg;

//画像エフェクト用
float addValImg;
float alphaImgCntMax = 1.0 * FR;  //エフェクト時間
float alphaImg;
float alphaImgCnt = alphaImgCntMax;

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

void draw() {
  background(0, 0, 0);
              
//--------------画像アニメーション用--------------//
  if(cntImg <= alphaImgCntMax){
    addValImg = -1;    //フェードイン
  }else if(cntImg >= imgTime - alphaImgCntMax){
    addValImg = 1;    //フェードアウト
  }
  
  strImg = "img/img" + String.valueOf(imgNum) + ".jpg";
  img = loadImage(strImg);
  imgW = 700;
  imgH = img.height * (imgW / img.width);
  imgX = width/3 - 400;
  imgY = height/2 - imgH/2;
  
  image(img, imgX, imgY, imgW, imgH);
  imgAnimation();
  
  cntImg ++;

  if(cntImg >= imgTime){
    imgNum ++;
    if(imgNum > imgNumMax){
      imgNum = 1;
    }
    cntImg = 0;
  }
  
  if(imgNum != imgNumMax){
    saveFrame( "frames/#####.png" );
  }
}

private void imgAnimation(){
  // 画像をフェードイン・フェードアウト
  alphaImgCnt += addValImg; 
  if( alphaImgCnt < 0 ){
    alphaImgCnt = 0;
  } else if( alphaImgCnt > alphaImgCntMax ){
    alphaImgCnt = alphaImgCntMax;
  }  
  alphaImg = (float)map(alphaImgCnt,0,alphaImgCntMax,0,255);  //透過率
  
  fill(0,0,0,alphaImg);  // 透過率alphaImgの黒色
  rect(imgX-5, imgY-5, imgW+10, imgH+10);  // 画像サイズよりも少し大きい長方形を描画
}
```
![out2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/fdcdb02a-7cb1-1c11-2dcc-b0eaaa3b012d.gif)

## プログラミング全文
```java
//参列者一覧
//"$"を加えると名前に"様"が付かなくなる
String[] GroomGuests = {
  "新郎A",
  "新郎B",
  "新郎C",
  "新郎D",
  "新郎E",
  "新郎F",
  "新郎G",
  "新郎H",
  "新郎I",
  "新郎J",
  "新郎K",
  "新郎L",
  "新郎M",
  "$新郎父",
  "$新郎母"
};

String[] BrideGuests = {
  "新婦A",
  "新婦B",
  "新婦C",
  "新婦D",
  "新婦E",
  "新婦F",
  "新婦G",
  "新婦H",
  "新婦I",
  "新婦J",
  "新婦K",
  "新婦L",
  "新婦M",
  "$新婦父",
  "$新婦母"
};

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

//文字スクロール用
float guestMoveStrSize = 50;    //ゲスト名の文字のサイズ
float scrollSpeed = 3;        //文字が流れる速さ
int   guestStrMargin = 90;      //エンドロール行間
int   groomBrideMargin = 50;    //新郎リスト - 新婦リスト行間

//テキスト表示用
PFont font;
String str;
float strX;
float strY;
float strSize;
int strAlign;

//画像表示用
float imgTime = 3 * FR;  //画像表示時間
int imgNum = 1;      // 画像番号の最小値
int imgNumMax = 10;  // 画像番号の最大値
PImage img;
float imgW;
float imgH;
float imgX;
float imgY;
float cntImg = 0;
String strImg;

//画像エフェクト用
float addValImg;
float alphaImgCntMax = 1.0 * FR;  //エフェクト時間
float alphaImg;
float alphaImgCnt = alphaImgCntMax;

//スクリーンフェードアウト
float alphaScreenCnt;
float alphaScreen;
float alphaScreenCntMax = 2.0 * FR;

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

void draw() {
  background(0, 0, 0);
  
  switch(flag){
    case 1:
      //--------------画像表示--------------//
      strImg = "img/img" + String.valueOf(imgNum) + ".jpg";
      img = loadImage(strImg);
      imgW = 700;
      imgH = img.height * (imgW / img.width);
      imgX = width/3 - 400;
      imgY = height/2 - imgH/2;
      
      image(img, imgX, imgY, imgW, imgH);
  
      //--------------画像アニメーション用--------------//
      if(cntImg <= alphaImgCntMax){
        addValImg = -1;    //フェードイン
      }else if(cntImg >= imgTime - alphaImgCntMax){
        addValImg = 1;    //フェードアウト
      }
      imgAnimation();
      
      cntImg ++;
    
      if(cntImg >= imgTime){
        imgNum ++;
        if(imgNum > imgNumMax){
          imgNum = 1;
        }
        cntImg = 0;
      }
      
      //--------------文字スクロール--------------//
      fill(255, 255);  // 文字色 白
      strSize = guestMoveStrSize;
      strX = 1200;
      strAlign = CENTER;
      textSize(strSize);    //文字列のサイズ
      textAlign(strAlign);  //文字列中央寄せ
      
      for(int i=-2; i<GroomGuests.length; i++){
        if(i==-2){
          font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");
          str = "GROOM's Guest";
        } else if(i==-1){
          font = createFont("Serif.bold", 50);
          str = "";  //1行分スペース
        } else{  // i=0以降
          font = createFont("Serif.bold", 50);
          if(GroomGuests[i].contains("$")){
            //$マークがついていれば様を付けない
            str = GroomGuests[i].replace("$", "");
          }else{
            str = GroomGuests[i] + " 様";
          }
        }
        
        // カウントアップとともにリストの文字列Y座標が小さくなる（上方向に移動する）
        // リスト内の文字列を(i * guestStrMargin)によって並べて表示
        strY = height + (i * guestStrMargin) - (cnt * scrollSpeed);
        
        //文字のY座標が画面内であれば文字を表示する
        if(strY > -10 || strY < height + 10){
          textFont(font);
          text(str, strX, strY);
        }
      }
      
      for(int i=-2; i<BrideGuests.length; i++){
        if(i==-2){
          font = loadFont("CourierNewPS-BoldItalicMT-48.vlw");
          str = "Bride's Guest";
        } else if(i==-1){
          font = createFont("Serif.bold", 50);
          str = "";  //1行分スペース
        } else{  // i=0以降
          font = createFont("Serif.bold", 50);
          if(BrideGuests[i].contains("$")){
            //$マークがついていれば様を付けない
            str = BrideGuests[i].replace("$", "");
          }else{
            str = BrideGuests[i] + " 様";
          }
        }
        
        // カウントアップとともにリストの文字列Y座標が小さくなる（上方向に移動する）
        // 
        strY = height + (GroomGuests.length + 2 + i) * guestStrMargin + groomBrideMargin - (cnt * scrollSpeed);
        if(strY > -10 || strY < height + 10){
          textFont(font);
          text(str, strX, strY);
        }
      }
      
      //画面全体をフェードアウト
      if(strY <= -100 + scrollSpeed * alphaScreenCntMax){
         alphaScreenCnt += 1; 
         if( alphaScreenCnt < 0 ){
         alphaScreenCnt = 0;
       } else if( alphaScreenCnt > alphaScreenCntMax ){
         alphaScreenCnt = alphaScreenCntMax;
       }  
       alphaScreen = (float)map(alphaScreenCnt,0,alphaScreenCntMax,0,255);
  
       fill(0,0,0,alphaScreen);
       rect(0, 0, width, height);
      }
            
      //スクロール終了判別 最後の行のY座標が-100になれば終了
      if(strY <= -100){
        flag = 2;
      }
  }
  
  cnt ++;  // ループごとにカウントアップ
  
  if(flag != flagMax){
    saveFrame( "frames/#####.png" );
  }
}

private void imgAnimation(){
  // 画像をフェードイン・フェードアウト
  alphaImgCnt += addValImg; 
  if( alphaImgCnt < 0 ){
    alphaImgCnt = 0;
  } else if( alphaImgCnt > alphaImgCntMax ){
    alphaImgCnt = alphaImgCntMax;
  }  
  alphaImg = (float)map(alphaImgCnt,0,alphaImgCntMax,0,255);  //透過率
  
  fill(0,0,0,alphaImg);  // 透過率alphaImgの黒色
  rect(imgX-5, imgY-5, imgW+10, imgH+10);  // 画像サイズよりも少し大きい長方形を描画
}
```

# 参考
loadImage(), image()
https://yoppa.org/proga10/1353.html