LovyanGFXでPartyParrotを動かそう(M5Stack)

なぜPartyParrotを作ろうと思ったのかはこちらのツイートが発端です

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">三次ベジェ曲線テスト…一応描けてはいるようだ <a href="https://t.co/5kbsP3xvZ1">pic.twitter.com/5kbsP3xvZ1</a></p>&mdash; らびやん (@lovyan03) <a href="https://twitter.com/lovyan03/status/1279589709505683463?ref_src=twsrc%5Etfw">July 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**なんかPartyParrotの動きに似てるな・・・(そうか？)**

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/LovyanGFX?src=hash&amp;ref_src=twsrc%5Etfw">#LovyanGFX</a> unstableブランチ更新。三次ベジェ曲線のdrawBezier関数を追加しました。<br>二次ベジェ曲線 ：引数の x y のペアが3つ<br>三次ベジェ曲線： 引数の x y のペアが4つ<br>最初と最後が端点の座標になり、中間の座標が制御点になります。 <a href="https://t.co/JTtuDPlYxu">pic.twitter.com/JTtuDPlYxu</a></p>&mdash; らびやん (@lovyan03) <a href="https://twitter.com/lovyan03/status/1279731903839395841?ref_src=twsrc%5Etfw">July 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こちらの3次ベジェ曲線追加のツイートでこれはいけそうだなと思って着手しました

##LovyanGFXをインストールする##

LovyanGFXとは
ESP32/SAMD51とSPI接続のLCDの組み合わせで動作するグラフィックライブラリです。
AdafruitGFX や TFT_eSPI と互換性をある程度持ちつつ、より高機能・高速動作を目標としています。(公式Githubより引用)

ライブラリマネージャでLovyanと検索すると出てきます

##PartyParrotを描画する##

ベジェ曲線を使用してPartyParrotを描画します
結構大変なのでいろいろと省略してます
後ほどアニメーションさせるのでspriteを使用します

```cpp
#include <LovyanGFX.h>

static LGFX lcd;                // LGFXのインスタンスを作成。
static LGFX_Sprite sprite(&lcd); // スプライトを使う場合はLGFX_Spriteのインスタンスを作成。

void drawSpriteParrot() {
        sprite.clear();
	sprite.setColor(lcd.color332(255, 255, 255));
	sprite.drawBezier(225, 70, 200, 80, 225, 150); // クチバシ左半分
	sprite.drawBezier(225, 70, 250, 80, 225, 150); // クチバシ右半分
	sprite.drawBezier(40, 240, 160, 210, 110, 40, 220, 40); // 左下から頭の上まで
	sprite.drawBezier(220, 40, 280, 40, 300, 120); // 頭の上から右頬まで
	sprite.drawBezier(300, 120, 310, 180, 270, 200, 290, 240); // 右頬から右下まで
	sprite.fillEllipse(260, 80, 10, 15); // 右目
	sprite.fillEllipse(190, 80, 10, 15); // 左目
	sprite.pushSprite(0, 0);
}


void setup(void) {
	Serial.begin(115200);
	lcd.init();

	lcd.setRotation(1);

	lcd.setBrightness(200);

	sprite.setPsram(true);
	sprite.setColorDepth(8);
	sprite.createSprite(320, 240);
	lcd.clear();
}

void loop(void) {
    	drawSpriteParrot();
	delay(1000);
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/ebda9eb4-ccb5-2ddc-5353-e3ebdd316f5c.png)

これを基本形とします

##PartyParrotをアニメーションさせる##

とりあえず細かい再現は抜きとして動かすことにしました

要点としては**右下と左下の点以外をすべて楕円運動させる**というだけです

楕円運動させればいいということでラジアンの変数を作り０～２までループさせることにしました

```cpp
float rad = 0;
float delta = 0.05;

uint8_t r, g, b;

int dx = 0, dy = 0;

void loop(void) {

	dx = cos(rad * 3.14) * 60 - 60;
	dy = sin(rad * 3.14) * -20;
	delay(1);
	rad += delta;
	if (2 < rad) {
		rad = 0;
	}
}

```
dxは0~-120、dyは20~-20の間で楕円運動します
(基本形が右中央から始まるため)

ここで計算したdx,dyを描画関数に渡します

```cpp

void drawSpriteParrot(int pdx, int pdy, uint8_t r, uint8_t g, uint8_t b) {
	sprite.clear();
	sprite.setColor(lcd.color332(255, 255, 255));
	sprite.drawBezier(225 + pdx, 70 + pdy, 200 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	sprite.drawBezier(225 + pdx, 70 + pdy, 250 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	// 左下の点のみ動かさない
	sprite.drawBezier(40, 240, 160 + pdx, 210 + pdy, 110 + pdx, 40 + pdy, 220 + pdx, 40 + pdy);
	sprite.drawBezier(220 + pdx, 40 + pdy, 280 + pdx, 40 + pdy, 300 + pdx, 120 + pdy);
	// 右下の点のみ動かさない
	sprite.drawBezier(300 + pdx, 120 + pdy, 310 + pdx, 180 + pdy, 270 + pdx, 200 + pdy, 290, 240);
	sprite.fillEllipse(260 + pdx, 80 + pdy, 10, 15);
	sprite.fillEllipse(190 + pdx, 80 + pdy, 10, 15);
	sprite.pushSprite(0, 0);
}
```

こうすることで動いてる頭と動かない肩への線がベジェ曲線で滑らかにつながることで
違和感のないアニメーションができます
(実際のPartyParrotはそこまで単純ではないがここでは省略)
spriteを使用しているのでちらつきもありません

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ちょっとティアリングしてますけど滑らかになりましたよ～＼(^o^)／<br><br>これから色換えを試してみます <a href="https://t.co/uOqKeTnDsN">pic.twitter.com/uOqKeTnDsN</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1280460402099253257?ref_src=twsrc%5Etfw">July 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##PartyParrotの色を変更する##

線画だと寂しいので色を変えます
先ほど使ったラジアンを使いRGBを1/3周期ずつずらしてループさせます

```cpp
	r = abs(sin(rad * 3.14)) * 255;
	g = abs(sin((rad + 0.6) * 3.14)) * 255;
	b = abs(sin((rad + 1.2) * 3.14)) * 255;
```

そのRGBを領域内を塗りつぶす
sprite.floodFill( std::int32_t x, std::int32_t y, const T& color)
に渡します

```cpp

void drawSpriteParrot2(int pdx, int pdy, uint8_t r, uint8_t g, uint8_t b) {
	sprite.clear();
	sprite.setColor(lcd.color332(255, 255, 255));
	sprite.drawBezier(225 + pdx, 70 + pdy, 200 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	sprite.drawBezier(225 + pdx, 70 + pdy, 250 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	sprite.drawBezier(40, 240, 160 + pdx, 210 + pdy, 110 + pdx, 40 + pdy, 220 + pdx, 40 + pdy);
	sprite.drawBezier(220 + pdx, 40 + pdy, 280 + pdx, 40 + pdy, 300 + pdx, 120 + pdy);
	sprite.drawBezier(300 + pdx, 120 + pdy, 310 + pdx, 180 + pdy, 270 + pdx, 200 + pdy, 290, 240);
	sprite.floodFill(120, 220, lcd.color332(r, g, b)); // オウムの内側を塗りつぶす
	sprite.setColor(lcd.color332(0, 0, 0));
        // 塗りつぶした後目を黒で描く
	sprite.fillEllipse(260 + pdx, 80 + pdy, 10, 15);
	sprite.fillEllipse(190 + pdx, 80 + pdy, 10, 15);
	sprite.pushSprite(0, 0);
}

```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!! <a href="https://t.co/cj4wDhQLlM">pic.twitter.com/cj4wDhQLlM</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1280487833900183555?ref_src=twsrc%5Etfw">July 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これで完成です！

##ソースコード##

おまけ機能を付けておいたのでA(左)ボタンを押してみてください

ちなみにdeltaを変更するとスピードを変えられますが、
FPSが頭打ちになるので実際のGIFと同じスピードにはなりません(´・ω・`)

<details><summary>M5 PartyParrot</summary>
<div>

```cpp

#include <Arduino.h>

#include <LovyanGFX.h>

static LGFX lcd;                // LGFXのインスタンスを作成。
static LGFX_Sprite sprite(&lcd); // スプライトを使う場合はLGFX_Spriteのインスタンスを作成。

// M5Stack
#define BUTTON_A_PIN 39
#define BUTTON_B_PIN 38

void drawSpriteParrot2(int pdx, int pdy, uint8_t r, uint8_t g, uint8_t b) {
	sprite.clear();
	sprite.setColor(lcd.color332(255, 255, 255));
	sprite.drawBezier(225 + pdx, 70 + pdy, 200 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	sprite.drawBezier(225 + pdx, 70 + pdy, 250 + pdx, 80 + pdy, 225 + pdx, 150 + pdy);
	sprite.drawBezier(40, 240, 160 + pdx, 210 + pdy, 110 + pdx, 40 + pdy, 220 + pdx, 40 + pdy);
	sprite.drawBezier(220 + pdx, 40 + pdy, 280 + pdx, 40 + pdy, 300 + pdx, 120 + pdy);
	sprite.drawBezier(300 + pdx, 120 + pdy, 310 + pdx, 180 + pdy, 270 + pdx, 200 + pdy, 290, 240);
	sprite.floodFill(120, 220, lcd.color332(r, g, b));
	sprite.setColor(lcd.color332(0, 0, 0));
	sprite.fillEllipse(260 + pdx, 80 + pdy, 10, 15);
	sprite.fillEllipse(190 + pdx, 80 + pdy, 10, 15);
	sprite.pushSprite(0, 0);
}

void drawSpriteBokyou(int pdx, int pdy, uint8_t r, uint8_t g, uint8_t b) {
	sprite.clear();

	sprite.fillRect(215 + pdx, 70 + pdy, 10, 80, lcd.color332(r, g, b));

	sprite.setColor(lcd.color332(255, 255, 255));

	sprite.drawBezier(40, 240, 160 + pdx, 210 + pdy, 110 + pdx, 40 + pdy, 220 + pdx, 40 + pdy);
	sprite.drawBezier(220 + pdx, 40 + pdy, 280 + pdx, 40 + pdy, 300 + pdx, 120 + pdy);
	sprite.drawBezier(300 + pdx, 120 + pdy, 310 + pdx, 180 + pdy, 270 + pdx, 200 + pdy, 290, 240);
	sprite.pushSprite(0, 0);
}

void setup(void) {
	Serial.begin(115200);
	  lgfx::lgfxPinMode(BUTTON_A_PIN, lgfx::pin_mode_t::input);
	  lgfx::lgfxPinMode(BUTTON_B_PIN, lgfx::pin_mode_t::input);
	lcd.init();

	lcd.setRotation(1);

	lcd.setBrightness(200);

	sprite.setPsram(true);
	sprite.setColorDepth(8);
	sprite.createSprite(320, 240);
	lcd.clear();
}

float rad = 0;
float delta = 0.05;

uint8_t r, g, b;

int dx = 0, dy = 0;

bool bokyou = false;

void loop(void) {
    static bool pressed;
    if (BUTTON_A_PIN >=0 && lgfx::gpio_in(BUTTON_A_PIN) == 0)
    {
      if (!pressed)
      {
        pressed = true;
        bokyou = !bokyou;
      }
    }
    else
    {
      pressed = false;
    }

	r = abs(sin(rad * 3.14)) * 255;
	g = abs(sin((rad + 0.6) * 3.14)) * 255;
	b = abs(sin((rad + 1.2) * 3.14)) * 255;

	dx = cos(rad * 3.14) * 60 - 60;
	dy = sin(rad * 3.14) * -20;
	if (bokyou) {
		drawSpriteBokyou(dx, dy, r, g, b);
	}
	else {
    	drawSpriteParrot2(dx, dy, r, g, b);
	}
	delay(1);
	rad += delta;
	if (2 < rad) {
		rad = 0;
	}
}

```

</div>
</details>