電子ペーパーデバイス 2種に p5.js で描画した画像を表示させてみる【Processing】（Adventar）

この記事は [Adventar](https://adventar.org/) でのアドベントカレンダーの 1つとして作られている、「[Processing Advent Calendar 2023](https://adventar.org/calendars/8532)」の 11日目の記事です。

## 今回の内容
この記事では、以下の 2種の電子ペーパーデバイスに p5.js で作った画像を表示させてみようというものです。

- 「EZ Door Sign」： 赤・黒・白の 3色が使える小型の電子ペーパーデバイス
- 「M5Paper」： スマホサイズの通信機能なども着いた電子ペーパーデバイス

どちらも電子ペーパーなので、いったん自分が出したい内容を表示させた状態にしてしまえば、書きかえをするタイミングまでは電源不要で表示が維持されます。
そのため、以前から所有していた「M5Paper」は、この特性がぴったりな、イベント用の名札の用途でよく使っています。

https://twitter.com/youtoy/status/1736244044861505621
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/3280e451-a9be-bb9b-d0be-21aee3018637.png)

### 2つの電子ペーパーの表示の書きかえ方に関する記事
上で挙げた 2つのデバイスについて、最近ゲットしたばかりの「EZ Door Sign」のほうは基本の使い方を記事に書きました。また、以前から持っている「M5Paper」のほうは、自分が重宝している用途の電子ペーパー名札として使う場合の「画像作成〜表示の書きかえ」を行う手順を記事にしました（※ 自分がやっている手順の紹介という内容）。

それぞれの記事の URL などは以下です。

●赤・黒・白の 3色が使える小型の電子ペーパーデバイス「EZ Door Sign」で画像を表示させる【完走賞ゲット-25】 - Qiita
https://qiita.com/youtoy/items/41ad3f38df7eb020a90e

●イベントで大活躍する電子ペーパー名札を作る！（M5Paper と UIFlow などで作成する手順） - Qiita
https://qiita.com/youtoy/items/545cb81fd3188e63f8aa

## デバイスに表示できる画像の仕様
上で紹介した過去記事にも書いている内容ではありますが、今回の内容を進めるのに必要な、表示に用いる画像の縦横のサイズについて記載します。

- 「EZ Door Sign」： 296 x 128 （横向きで使う場合）
- 「M5Paper」： 540 x 960 （縦向きで使う場合）

このサイズの画像を出力するように作っていきます。

## p5.js で表示用画像を保存する
p5.js で電子ペーパーデバイス用の画像を保存する処理について、利用する仕組みについて軽く触れておきます。

### キャンバスのピクセル密度の指定
電子ペーパーデバイス用の画像の縦横のサイズは、 `createCanvas()` で指定しますが、その際に[ピクセル密度の指定](https://p5js.org/reference/#/p5/pixelDensity)も合わせて行っておきます。具体的には `pixelDensity(1)` と指定することで、キャンバスの縦横のサイズが、そのまま後で出力する画像の縦横のサイズに反映されるようにします。

### 画像の保存
画像の保存は、saveCanvas() を使います。

●reference | saveCanvas()
　https://p5js.org/reference/#/p5/saveCanvas

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2f1398b9-5a31-8125-c443-9ba23b00b59f.png)

引数は 2つ指定する形で、 `saveCanvas("（ファイル名）", "jpg")` という内容にします。
これを、キー押下時に実行するような形にしていきます。

### その他の部分
電子ペーパーデバイス用に行う処理は上記のとおりです。
あとは、表示内容を作成する描画部分を作る形です。

## 表示させる画像を作成してデバイスで表示させる
それでは、実際にデバイスに表示させる部分を作っていきます。

### 「M5Paper」で試す
まずは、2つあるデバイスのうち「M5Paper」のほうで試してみます。

表示する内容は、過去に作った以下を流用することにしました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/oIIM3jgwoZQ?si=TXibLRUFE_k6qmw8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

キャンバスのサイズを変えたり、電子ペーパーデバイス用の画像出力を行う部分を追加したりします。
その対応を行った後のものは、以下の通りです。

```javascript
const colorArray = [
  "#3d348b",
  "#7678ed",
  "#7678ed",
  "#f7b801",
  "#f18701",
  "#f35b04",
];

const circleArray = [],
  circleNum = 16;
const stepNum = 10;

function setup() {
  pixelDensity(1);
  const canvasWidth = 540;
  createCanvas(540, 960);
  noFill();

  for (let i = 0; i < circleNum; i++) {
    const randomColorArray = Array(stepNum)
      .fill()
      .map(() => random(colorArray) + hex(random(80, 180), 2));

    const randomWeightArray = Array(stepNum)
      .fill()
      .map(() => random(3, 5));

    const randomAnglesArray = Array.from({ length: 10 }, () => {
      return random() < 0.3 ? [0, TAU] : [random(TAU), random(TAU)];
    });
    const randomRotationParamArray = Array.from({ length: 10 }, () =>
      random([0.05, 0.2, 0.3, 0.4, 0.5, 0.7])
    );

    circleArray.push({
      x: random(100, width - 100),
      y: random(100, height - 100),
      maxR: random(80, 200),
      step: stepNum,
      randomColorArray: randomColorArray,
      randomWeightArray: randomWeightArray,
      randomAnglesArray: randomAnglesArray,
      randomRotationParamArray: randomRotationParamArray,
      vx: random(-5, 5),
      vy: random(-5, 5),
    });
  }
}

function draw() {
  background("#F8D46A");

  for (let c of circleArray) {
    c.x += c.vx;
    c.y += c.vy;

    if (c.x - c.maxR / 2 < 0 || c.x + c.maxR / 2 > width) {
      c.vx = -c.vx;
    }
    if (c.y - c.maxR / 2 < 0 || c.y + c.maxR / 2 > height) {
      c.vy = -c.vy;
    }

    for (c of circleArray) {
      const dA = frameCount / 2;

      for (let i = 0; i < stepNum; i++) {
        stroke(c.randomColorArray[i]);
        strokeWeight(c.randomWeightArray[i]);
        const r = (i + 1) * (c.maxR / c.step);

        arc(
          c.x,
          c.y,
          r,
          r,
          c.randomAnglesArray[i][0] + dA * c.randomRotationParamArray[i],
          c.randomAnglesArray[i][1] + dA * c.randomRotationParamArray[i]
        );
      }
    }
  }
}

function keyPressed() {
  if (key == "s") {
    saveCanvas("m5", "jpg"); // ファイル名, ファイル形式
  }
}
```

上記の処理で描画を行い、その途中で保存した画像は以下のとおりです。

![m5 (5).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ad4345dd-79da-6cd3-69e8-317a34c99ded.jpeg)

あとは、これを M5Paper に表示させてみます。
デバイスへの画像のアップロードには、自分がいつも使っている UIFlow を使うのですが、その場合は画像のファイルサイズを削減する必要があります。

その対応は、単純に画像の圧縮率を高めることで行います。

#### 表示結果（M5Paper）
デバイスに表示してみた結果は、以下のとおりです。

https://twitter.com/youtoy/status/1739262504516342169
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/94e812e8-0405-6157-4b37-3ea27942bd58.png)

今回、とりあえず過去に作った作品を 1つピックアップして、電子ペーパーデバイスで表示する画像を作成するのに使いましたが、実際にデバイスに表示を行ってみたところ、この M5Paper の表示サイズ・縦横比を考慮した描画を考えて、このデバイスで良い感じに見えるものを作ってみたいと思いました。

### 「EZ Door Sign」で試す
先ほどと同じ内容をベースに、「EZ Door Sign」のほうでも試してみます。

今回用いるプログラムは、以下のとおりです。

```javascript
const colorArray = [
  "#3d348b",
  "#7678ed",
  "#7678ed",
  "#f7b801",
  "#f18701",
  "#f35b04",
];

const circleArray = [],
  circleNum = 6;
const stepNum = 10;

function setup() {
  pixelDensity(1);
  frameRate(5)
  
  const canvasWidth = 540;
  createCanvas(296, 128);
  noFill();

  for (let i = 0; i < circleNum; i++) {
    const randomColorArray = Array(stepNum)
      .fill()
      .map(() => random(colorArray) + hex(random(80, 180), 2));

    const randomWeightArray = Array(stepNum)
      .fill()
      .map(() => random(3, 5));

    const randomAnglesArray = Array.from({ length: 10 }, () => {
      return random() < 0.3 ? [0, TAU] : [random(TAU), random(TAU)];
    });
    const randomRotationParamArray = Array.from({ length: 10 }, () =>
      random([0.05, 0.2, 0.3, 0.4, 0.5, 0.7])
    );

    circleArray.push({
      x: random(100, width - 100),
      y: random(100, height - 100),
      maxR: random(80, 200),
      step: stepNum,
      randomColorArray: randomColorArray,
      randomWeightArray: randomWeightArray,
      randomAnglesArray: randomAnglesArray,
      randomRotationParamArray: randomRotationParamArray,
      vx: random(-5, 5),
      vy: random(-5, 5),
    });
  }
}

function draw() {
  background("#F8D46A");

  for (let c of circleArray) {
    c.x += c.vx;
    c.y += c.vy;

    if (c.x - c.maxR / 2 < 0 || c.x + c.maxR / 2 > width) {
      c.vx = -c.vx;
    }
    if (c.y - c.maxR / 2 < 0 || c.y + c.maxR / 2 > height) {
      c.vy = -c.vy;
    }

    for (c of circleArray) {
      const dA = frameCount / 2;

      for (let i = 0; i < stepNum; i++) {
        stroke(c.randomColorArray[i]);
        strokeWeight(c.randomWeightArray[i]);
        const r = (i + 1) * (c.maxR / c.step);

        arc(
          c.x,
          c.y,
          r,
          r,
          c.randomAnglesArray[i][0] + dA * c.randomRotationParamArray[i],
          c.randomAnglesArray[i][1] + dA * c.randomRotationParamArray[i]
        );
      }
    }
  }
}

function keyPressed() {
  if (key == "s") {
    saveCanvas("m5", "jpg"); // ファイル名, ファイル形式
  }
}
```

上記の処理で描画を行い、その途中で保存した画像は以下のとおりです。

![m5 (5).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0c38eb70-b2e1-f573-9513-0c5b8ef3d454.jpeg)

これを「EZ Door Sign」で表示させてみます。

#### 表示結果（EZ Door Sign）
デバイスに表示してみた結果は、以下のとおりです。

https://twitter.com/youtoy/status/1739277470900351368
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/44d0e4f4-653d-c782-0813-71574d54b9b1.png)

こちらの EZ Door Sign のほうも、上記の M5Paper と同様に、3色の割り当てが行われる前提の描画を行えると良いと思いました（具体的な内容は思いついてないですが...）。
EZ Door Sign の場合、白・黒・その中間色の 3つで描画をして、それぞれが 表示に使われる 3色に割り当てられる想定で描画しておくと良さそうに思いました。

## おわりに
今回、「M5Paper」「EZ Door Sign」という 2種類の電子ペーパーデバイスに、p5.js で描画した内容を表示させてみました。

今回使った「M5Paper」と「EZ Door Sign」で、表示の特性も表示領域の大きさも違うので、それらを考慮した、「M5Paper」・「EZ Door Sign」への表示用の描画というのを作れたら良いと思いました。