なぜPartyParrotを作ろうと思ったのかはこちらのツイートが発端です
三次ベジェ曲線テスト…一応描けてはいるようだ pic.twitter.com/5kbsP3xvZ1
— らびやん (@lovyan03) July 5, 2020
なんかPartyParrotの動きに似てるな・・・(そうか?)
#LovyanGFX unstableブランチ更新。三次ベジェ曲線のdrawBezier関数を追加しました。
— らびやん (@lovyan03) July 5, 2020
二次ベジェ曲線 :引数の x y のペアが3つ
三次ベジェ曲線: 引数の x y のペアが4つ
最初と最後が端点の座標になり、中間の座標が制御点になります。 pic.twitter.com/JTtuDPlYxu
こちらの3次ベジェ曲線追加のツイートでこれはいけそうだなと思って着手しました
##LovyanGFXをインストールする##
LovyanGFXとは
ESP32/SAMD51とSPI接続のLCDの組み合わせで動作するグラフィックライブラリです。
AdafruitGFX や TFT_eSPI と互換性をある程度持ちつつ、より高機能・高速動作を目標としています。(公式Githubより引用)
ライブラリマネージャでLovyanと検索すると出てきます
##PartyParrotを描画する##
ベジェ曲線を使用してPartyParrotを描画します
結構大変なのでいろいろと省略してます
後ほどアニメーションさせるのでspriteを使用します
#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);
}
これを基本形とします
##PartyParrotをアニメーションさせる##
とりあえず細かい再現は抜きとして動かすことにしました
要点としては右下と左下の点以外をすべて楕円運動させるというだけです
楕円運動させればいいということでラジアンの変数を作り0~2までループさせることにしました
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を描画関数に渡します
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を使用しているのでちらつきもありません
ちょっとティアリングしてますけど滑らかになりましたよ~\(^o^)/
— もけ@ムギ㌠ (@coppercele) July 7, 2020
これから色換えを試してみます pic.twitter.com/uOqKeTnDsN
##PartyParrotの色を変更する##
線画だと寂しいので色を変えます
先ほど使ったラジアンを使いRGBを1/3周期ずつずらしてループさせます
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)
に渡します
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);
}
キタ━━━━(゚∀゚)━━━━!! pic.twitter.com/cj4wDhQLlM
— もけ@ムギ㌠ (@coppercele) July 7, 2020
これで完成です!
##ソースコード##
おまけ機能を付けておいたのでA(左)ボタンを押してみてください
ちなみにdeltaを変更するとスピードを変えられますが、
FPSが頭打ちになるので実際のGIFと同じスピードにはなりません(´・ω・`)
M5 PartyParrot
#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;
}
}