Processingで作るプレゼンスライド

# はじめに
こんにちは，ohayotaです．
プレゼンスライドをProcessingで作りたいと思ったこと，ありますよね（？）
先日，[琉大と未来大の合同LT][1]で登壇する際のスライドを**Processingで作って，Processingでプレゼンしてみました．**
そこでこの記事では，どのようにスライドを作ったか書いていこうと思います．
[1]:https://connpass.com/event/133298/

# この記事を読む上で必要なもの
- Processing（Java）の文法理解
- オブジェクト指向（クラスなど）の文法理解
- 集中力

# 作りたいスライドのイメージ
ここで作ろうとしているスライドは，以下の画像のような感じです．
### 表紙スライド
<img width="1680" alt="coverSlideImage.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/ad2a1725-9750-6a18-75bd-b0d28a21ba5c.png">
### 通常スライド
<img width="1680" alt="slideImage.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/68fff603-1bf3-dfc0-754c-ed8f9361c430.png">

この2つのイメージに合わせて実装していきます．
（Processingの基本的な説明は割愛します．分からない場合は[公式Reference][2]等をご参照ください）
[2]:https://processing.org/reference/

# 構成
スライドを作ると言っても，`rect(...)`や`text(...)`を`setup()`や`draw()`に一つずつ書いていくのは面倒です．
特に，数十枚もスライドを作りたいという場合は手間ですよね（少なくとも自分は嫌です）．
そこで，classを複数用意し**オブジェクト指向**で実装しました．

クラスの構成は，ざっくり以下の通りに決めました．

- **Slide**（スライド1枚を表すクラス）
    - **TopBar**（画面上部のエリアを表すクラス）
    - **TextField**（テキストや画像を表示するエリアを表すクラス）
        - **Text**（表示するテキストを表すクラス）
        - **CroppedImage**（円形にした画像を表示するクラス）
        - **Image**（四角形の画像を表示するクラス）
    - **BottomBar**（画面下部のエリアを表すクラス）
        - **CroppedImage**（円形にした画像を表示するクラス）

スライド1枚につき，**TopBar** & **TextField** & **BottomBar** が1組必要になります．

# 実装
クラスごとに実装していきます．
まずは下準備から．

## 下準備
実装前に，画面サイズや色，使用する画像やフォントなどを準備していきます．

```java
// 使うフォント
PFont yuGothic90;
PFont yuGothic70;
PFont yuGothic50;
PFont yuGothic30;
PFont yuGothic15;
PFont helvetica;
// 使う画像
PImage dummy600;

// createSlides()でスライド全部を入れる
ArrayList<Slide> slides = new ArrayList<Slide>();
String slideName = "スライド名";
int slideNum = 0;  // 表示対象のスライド番号

boolean isKeyTyped = false;

final color backColor = color(240);
final color mainColor = color(0, 0, 70);
final color subColor1 = color(100);
final color subColor2 = color(200);

void setup() {
  fullScreen();
  noStroke();
  noCursor();
  imageMode(CENTER);
  frameRate(15);
  
  yuGothic90 = createFont("YuGo-Bold", 90, true);
  yuGothic70 = createFont("YuGo-Bold", 70, true);
  yuGothic50 = createFont("YuGo-Bold", 50, true);
  yuGothic30 = createFont("YuGo-Bold", 30, true);
  yuGothic15 = createFont("YuGo-Bold", 15, true);
  helvetica = createFont("Helvetica-Bold", 50, true);
  
  dummy600 = loadImage("dummy600.png");
  
  createSlides();
}

void createSlides() {
  // スライド追加処理を書く
}
```

使う素材を準備し．`fillScreen()`で全画面表示にしておきます．
`createSlides()`は，Slideクラスが完成したら使います．

以下で扱うプログラム用に，600px * 600pxのダミー画像を用意します．
同じディレクトリ内に「data」というディレクトリを作成し，その中に`dummy600.png`という名前で保存します．
![dummy600.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/fc1c6356-4b20-9dbd-9400-31c4078ab3ee.png)

準備が出来たので，各クラスを実装していきます．
これ以降は，画面の縦横サイズを利用して**相対的にレイアウトを決めていきます．**

## TopBarクラス

<img width="1680" alt="slideTop.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/eafa9edf-ea3c-ca77-4ae6-eba5ae5692e6.png">


必要なものは以下の通りです．

- 紺色の背景
- タイトル
- サブタイトル
- スライド番号

これらを表示できるように実装します．

```java:TopBar.pde
class TopBar {

  float barHeight;
  String title;
  String subTitle;
  int number;

  TopBar(float barHeight, String title, String subTitle, int number) {
    this.barHeight = barHeight;
    this.title = title;
    this.subTitle = subTitle;
    this.number = number;
  }

  void drawBase() {
    noStroke();
    fill(mainColor);
    rect(0, 0, width, barHeight);
  }

  void drawTitle() {
    fill(backColor);
    textFont(yuGothic70);
    textSize(height/17.5);
    textAlign(LEFT, CENTER);
    text(title, width/33.6, barHeight/2);
  }

  void drawSubTitle() {
    fill(backColor);
    textFont(yuGothic30);
    textSize(height/35);
    textAlign(RIGHT, CENTER);
    text(subTitle, width*10.2/11.2, barHeight/2);
  }

  void drawNumber() {
    fill(backColor);
    textFont(helvetica);
    textSize(height/21);
    textAlign(RIGHT, CENTER);
    text(number, width*32.6/33.6, barHeight/2);
  }

  void draw() {
    drawBase();
    drawTitle();
    drawSubTitle();
    drawNumber();
  }

}
```

## Imageクラス

<img width="551" alt="imageSquare.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/cc121357-be03-e796-f77b-23ca95b25314.png">

上の例のように，1枚の四角形の画像を表示するクラスです．

```java:Image.pde
class Image {
  
  PImage image;
  float sizeX;
  float sizeY;
  float x;
  float y;
  color back;
  boolean isReflect;  // 上下左右を反転する必要があるか
  
  Image(PImage image, float sizeX, float sizeY, float x, float y, color back, boolean isReflect) {
    this.image = image;
    this.sizeX = sizeX;
    this.sizeY = sizeY;
    this.x = x;
    this.y = y;
    this.back = back;
    this.isReflect = isReflect;
  }
  
  void drawImage() {
    pushMatrix();
    translate(x, y);
    if (isReflect) rotate(radians(180));
    image(image, 0, 0, sizeX, sizeY);
    popMatrix();
  }
  
  void drawFrame() {
    noFill();
    strokeWeight(sizeY/100+2);
    stroke(mainColor);
    rect(x-sizeX/2, y-sizeY/2, sizeX, sizeY);
  }
  
  void draw() {
    drawImage();
    drawFrame();
  }
  
}
```

## CroppedImageクラス

<img width="553" alt="cimage.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/35278306-5a26-642c-55ba-84964723a3da.png">

このクラスの実装では，[Processingで丸い画像を表示する][3]で用いた方法を使います．
上の例のように，円形に切り取った1枚の画像を表示するクラスです．
[3]:https://qiita.com/ohayota/items/e00e29b2a86a8143f258

```java:CroppedImage.pde
class CroppedImage {
  
  PImage image;
  float size;
  float x;
  float y;
  color back;
  boolean isReflect;  // 上下左右を反転する必要があるか
  
  CroppedImage(PImage image, float size, float x, float y, color back, boolean isReflect) {
    this.image = image;
    this.size = size;
    this.x = x;
    this.y = y;
    this.back = back;
    this.isReflect = isReflect;
  }
  
  void drawBaseImage() {
    pushMatrix();
    translate(x, y);
    if (isReflect) rotate(radians(180));
    image(image, 0, 0, size, size);
    popMatrix();
  }
  
  // 正方形の中を円形に切り抜く
  void drawCroppedShape() {
    pushMatrix();
    translate(x, y);
    fill(back);
    noStroke();
    beginShape();
    // 図形の外枠
    vertex(-(size/2+1), -(size/2+1));
    vertex(size/2+1, -(size/2+1));
    vertex(size/2+1, size/2+1);
    vertex(-(size/2+1), size/2+1);
    //　切り抜く図形の描画
    beginContour();
    for (int i = 360; 0 < i; i--) {
      vertex(size/2 * cos(radians(i)), size/2 * sin(radians(i)));
    }
    endContour();
    endShape(CLOSE);
    popMatrix();
  }
  
  void drawFrame() {
    // 切り抜いた画像の枠を描画
    noFill();
    strokeWeight(size/100+2);
    stroke(mainColor);
    ellipse(x, y, size, size);
  }
  
  // 正確には画像自体を切り抜くのではなく，切り抜いた図形を画像の上に描画する
  void draw() {
    drawBaseImage();
    drawCroppedShape();
    drawFrame();
  }
  
}
```

## BottomBarクラス

<img width="1680" alt="slideBottom.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/cd459129-97b7-b5eb-163a-98e4f38130fc.png">

必要なものは以下の通りです．

- 濃い灰色の背景
- スライド名
- 発表者アイコンと名前

これらを表示できるように実装します．

```java:BottomBar.pde
class BottomBar {
  
  float barHeight;
  String title;
  CroppedImage image;
  
  BottomBar(float barHeight, String title) {
    this.barHeight = barHeight;
    this.title = title;
    image = new CroppedImage(dummy600, barHeight*2/3, width*7.5/8.4, height-barHeight/2, subColor1, false);
  }
  
  void drawBase() {
    noStroke();
    fill(subColor1);
    rect(0, height-barHeight, width, barHeight);
  }
  
  void drawTitle() {
    fill(subColor2);
    textFont(yuGothic30);
    textSize(height/35);
    textAlign(LEFT, CENTER);
    text(title, width/33.6, height-barHeight/2);
  }
  
  void drawUserInfo() {
    textSize(height/42);
    text("ユーザ名", image.x + image.size/2 + width/168, image.y - barHeight/8);
    textFont(yuGothic15);
    textSize(height/70);
    text("ユーザID など", image.x + image.size/2 + width/168, image.y + barHeight/6);
    image.draw();
  }
  
  void draw() {
    drawBase();
    drawTitle();
    drawUserInfo();
  }
  
}
```

## TextFieldクラス

<img width="1680" alt="slideField.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/9a04e2be-e03e-ff2a-3a45-283f18f75a34.png">

テキストや画像を表示するエリアを表すクラスです．
箇条書きのためにレベルを1から3まで設定します．
第2レベルには丸のマーカー，第3レベルには線のマーカーを左に表示します．

```java:TextField.pde
class TextField {
  
  float topBarHeight;
  float bottomBarHeight;
  float x;
  float y;
  float fieldWidth;
  float fieldHeight;
  ArrayList<CroppedImage> cImages;
  ArrayList<Image> images;
  ArrayList<Text> texts;
  
  TextField(float topBarHeight, float bottomBarHeight, float marginX, float marginY) {
    this.topBarHeight = topBarHeight;
    this.bottomBarHeight = bottomBarHeight;
    this.x = marginX;
    this.y = topBarHeight + marginY;
    fieldWidth = width - marginX*2;
    fieldHeight = height - (y+bottomBarHeight+marginY);
    cImages = new ArrayList<CroppedImage>();
    images = new ArrayList<Image>();
    texts = new ArrayList<Text>();
  }
  
  void drawImages() {
    for (CroppedImage img: cImages) img.draw();
    for (Image img: images) img.draw();
  }
  
  void drawTexts() {
    if (!texts.isEmpty()) {
      int textX = 0;
      int textY = 0;
      int beforeLevel = texts.get(0).level;
      for (int i = 0; i < texts.size(); i++) {
        Text t = texts.get(i);
        switch (t.level) {
          case 0:
            textX = 0;
            textY += height/17.5;
            break;
          case 1:
            textX = 0;
            t.draw(textX, textY);
            textY += height/15;
            break;
          case 2:
            if (beforeLevel == 3) {
              textX -= width/28;
            } else if (beforeLevel != 2) {
              textX += width/28;
            }
            t.draw(textX, textY);
            textY += height/17.5;
            break;
          case 3:
            if (beforeLevel != 3) textX += width/28;
            t.draw(textX, textY);
            textY += height/21;
            break;
          default:
        }
        beforeLevel = t.level;
      }
    }
  }
  
  void draw() {
    pushMatrix();
    // TextField内での位置を設定しているため，TextFieldの左上を原点(0, 0)とする
    translate(x, y);
    drawTexts();
    drawImages();
    popMatrix();
  }
  
}
```

## Textクラス

TextFieldクラス内で使うTextクラスです．
テキストの内容，レベル，フォント，マーカー，色などの情報を持ちます．

```java:Text.pde
class Text {
  
  String text;
  int level;
  
  Text(String text, int level) {
    this.text = text;
    this.level = level;
  }
  
  // 第2レベルなら丸，第3レベルなら線をマーカーとしてテキストの左側に表示
  void drawMarker(float x, float y) {
    switch (level) {
      case 2:
        fill(mainColor);
        noStroke();
        ellipse(x-width/48, y+height/52.5, height/42, height/42);
        break;
      case 3:
        stroke(mainColor);
        strokeCap(SQUARE);
        strokeWeight(height/262.5);
        line(x-width/33.6, y+height/70, x-width/60, y+height/70);
        break;
      default:
        return;
    }
  }
  
  void drawText(float x, float y) {
    switch (level) {
      case 1:
        fill(mainColor);
        textFont(yuGothic50);
        textSize(height/21);
        break;
      case 2:
        fill(subColor1);
        textFont(yuGothic50);
        textSize(height/26.25);
        break;
      case 3:
        fill(subColor1);
        textFont(yuGothic30);
        textSize(height/35);
        break;
      default:
        return;
    }
    textAlign(LEFT, TOP);
    text(text, x, y);
  }
  
  void draw(float x, float y) {
    drawMarker(x, y);
    drawText(x, y);
  }
  
}
```

## Slideクラス
1枚のスライドを表すクラスです．

```java:Slide.pde
class Slide {
  
  TopBar topBar;
  BottomBar bottomBar;
  TextField textField;
  int number;  // スライド番号
  boolean isCover;
  String title;
  String subTitle;
  
  Slide(boolean isCover, int number, String title, String subTitle) {
    topBar = new TopBar(height/8, title, subTitle, number);
    bottomBar = new BottomBar(height/12, slideName);
    textField = new TextField(topBar.barHeight, bottomBar.barHeight, width/21, height/17.5);
    this.isCover = isCover;
    this.number = number;
    this.title = title;
    this.subTitle = subTitle;
  }
  
  void drawTitle() {
    fill(mainColor);
    textFont(yuGothic90);
    textSize(height/11.7);
    textAlign(CENTER, TOP);
    text(title, width/2, height*4.5/13);
  }
  
  void drawSubTitle() {
    fill(subColor1);
    textFont(yuGothic50);
    textSize(height/21);
    textAlign(CENTER, TOP);
    text(subTitle, width/2, height*8.5/13);
  }
  
  void draw() {
    background(backColor);
    if (isCover) {
      drawTitle();
      drawSubTitle();
    } else {
      topBar.draw();
      bottomBar.draw();
      textField.draw();
    }
  }
  
}
```

## スライドの追加
スライドを構成するクラスを一通り実装したので，今度はスライドの各ページ（Slideクラスのインスタンス）を作っていきます．
下準備の段階で，`createSlides()`という関数がありましたね．
この関数内でスライドを追加します．（今回はダミーのデータです）

```java
void createSlides() {
  // 表紙
  slides.add(new Slide(true, 0, "タイトル", "サブタイトル"));
  // スライド1枚目
  Slide s1 = new Slide(false, 1, "タイトル", "サブタイトル");
  s1.textField.texts.add(new Text("第1レベル", 1));
  s1.textField.texts.add(new Text("第2レベル", 2));
  s1.textField.texts.add(new Text("第3レベル", 3));
  s1.textField.texts.add(new Text("第2レベル", 2));
  s1.textField.texts.add(new Text("第3レベル", 3));
  s1.textField.texts.add(new Text("", 0));  // 空白行
  s1.textField.texts.add(new Text("第1レベル", 1));
  s1.textField.texts.add(new Text("第2レベル", 2));
  s1.textField.texts.add(new Text("第2レベル", 2));
  s1.textField.texts.add(new Text("第3レベル", 3));
  s1.textField.texts.add(new Text("第3レベル", 3));
  s1.textField.cImages.add(new CroppedImage(dummy600, s1.textField.fieldWidth/3,
                                            s1.textField.x + s1.textField.fieldWidth*7/10,
                                            s1.textField.fieldHeight/2, backColor, false));
  slides.add(s1);
  // スライド2枚目
  Slide s2 = new Slide(false, 2, "タイトル", "サブタイトル");
  s2.textField.texts.add(new Text("第1レベル", 1));
  s2.textField.texts.add(new Text("第2レベル", 2));
  s2.textField.texts.add(new Text("第3レベル", 3));
  s2.textField.texts.add(new Text("第2レベル", 2));
  s2.textField.texts.add(new Text("第3レベル", 3));
  s2.textField.texts.add(new Text("", 0));  // 空白行
  s2.textField.texts.add(new Text("第1レベル", 1));
  s2.textField.texts.add(new Text("第2レベル", 2));
  s2.textField.texts.add(new Text("第2レベル", 2));
  s2.textField.texts.add(new Text("第3レベル", 3));
  s2.textField.texts.add(new Text("第3レベル", 3));
  s2.textField.images.add(new Image(dummy600, s2.textField.fieldWidth/3, s2.textField.fieldWidth/3,
                                    s2.textField.x + s2.textField.fieldWidth*7/10,
                                    s2.textField.fieldHeight/2, backColor, false));
  slides.add(s2);
  // スライド3枚目...
}
```

スライドのインスタンスを生成し，そのインスタンスの中にある`textField`にテキスト(**Text**)や画像(**Image**や**CroppedImage**)のインスタンスを追加します．
textFieldの追加が終わったら，`ArrayList<Slide> slides`にスライドのインスタンスを追加します．

## スライドの表示
スライドの追加が完了したので，`draw()`でスライドの`draw()`を呼びます．

```java
void draw() {
  background(backColor);
  slides.get(slideNum).draw();
}
```

これで，表紙スライドが表示されるようになりました．

## スライドの切替
プレゼンに使うためには，スライドを切り替えられる機能が必要です．
キーボードの左右矢印で切り替えられるようにします．

```java:keyEvent.pde
void keyPressed() {
  if (!isKeyTyped) {
    switch (keyCode) {
      case LEFT:
        if (0 < slideNum) {
          loop();
          slideNum--;
        }
        break;
      case RIGHT:
        if (slideNum < slides.size()-1) {
          loop();
          slideNum++;
        }
        break;
      default:
    }
    isKeyTyped = true;
  }
}

void keyReleased() {
  isKeyTyped = false;
}
```

キーが押されたときにループしていると，1回押しただけで複数スライドが切り替わる可能性があります．
そこで，`draw()`関数に`noLoop()`を追加します．

```java
void draw() {
  background(backColor);
  slides.get(slideNum).draw();

  noLoop();  // 特定のキーが押された時以外はループしないようにする
}
```
矢印キーが1回押された時に1枚ずつ切り替わるようになりました．
これで実装は完了です．

# おわりに
今回実装したスライドは自分専用のスライドマスターでしたが，プログラム中の色やレイアウトをカスタマイズすることでオリジナルのスライドを作ることができます．
ぜひチャレンジしてみてください．