Skip to main content

Seeed Studio Round Display for XIAO クリスマスボール

このチュートリアルでは、雪が降り、背景画像が変化するクリスマスボールの作り方を紹介します。

プログラムは以下の機能を実行します:

  • C配列として保存された背景画像を表示します。
  • 風の効果を伴って画像上に雪の粒子が降るシミュレーションを行います。
  • タッチ入力を検出し、背景画像のセットを循環表示します。
  • スムーズなアニメーションのためにダブルバッファリングを使用します。

環境準備

ハードウェア

このプロジェクトには以下が必要です:

メモリの関係でXIAO ESP32S3を使用しています。PNGDECの実行には約40kバイトのメモリが必要です。

ソフトウェア準備

Round Displayを使用するには、Getting Started with Round Display for XIAOにアクセスして必要なライブラリをインストールしてください。

すべてが正常に動作するかいくつかの例を試してみてください。

ライブラリ

このプロジェクトでは、Seeed Studio Round Display For XIAOにバンドルされているライブラリを使用します。

チュートリアルGetting Started with Round Display for XIAOで指定されているように、すべてのライブラリをインストールしてください。 その後、以下が必要です:

  • PNGdecライブラリ
  • LVGLライブラリの更新(またはSeeed Studio githubのものをインストールしない)

画像

画像はFlash配列に保存されたPNG画像です。PNGdecライブラリを使用して表示されます。

すべての画像はPNGである必要があります

以下は私が使用した画像です - すべてAI生成です

背景画像は、TFT_eSPIが表示でき、Round Display for XIAOによく適合するように準備する必要があります。

画像の準備

画像のリサイズ

Round Display for XIAOの解像度は240x240です。画像をリサイズする必要があります。GIMPを使用した方法を紹介します。

  1. 画像を開く
  2. Image > Scale Imageに移動
  1. 幅と高さを240に設定します。Keep Ratioが選択されている(チェーン)ため、widthを変更するとheightも変更されるはずです。
  1. Scaleボタンを押します。
  1. 画像を保存します(古いものを上書きします)

Flash配列の作成

注意: この手順はTFT_eSPI Flash_PNGの例に含まれています。

Flash配列を作成するには、File to C style array converterにアクセスしてください。

手順は以下の通りです:

  1. Browseを使用して画像をアップロードします。画像をアップロードした後
  1. いくつかのオプションを設定する必要があります
  • Treat as binary

他のすべてのオプションがグレーアウトします。

  1. Data typecharに変更しましょう
  1. convertを押します。これで画像が配列に変換されます。
  1. Save as fileボタンを押して画像を保存し、Arduinoスケッチに追加するか、Copy to clipboardボタンを押すことができます。 Copy to clipboardを選択した場合、Arduinoエディタの右側にある3つの点を押してNew Tabを選択する必要があります。

名前を付けます(通常は画像名に.h拡張子を付けたもの)

すべての画像が*.h*ファイルとして表示されます。

コード

以下はクリスマスボールのコードです。 コードの主要な機能について少し説明します。コードにはいくつかのコメントも含まれています。

ヘッダーとライブラリ

まず、いくつかのライブラリをインクルードします。

#include <PNGdec.h>
#include <TFT_eSPI.h>
#include <Wire.h>

#include "background1.h"
#include "background2.h"
#include "background3.h"

#define USE_TFT_ESPI_LIBRARY
#include "lv_xiao_round_screen.h"

Seeed Studioライブラリがインストールされている必要があることを覚えておいてください。

背景画像

背景画像を管理するための関数は以下の通りです

struct Background {
const uint8_t *data;
size_t size;
};

const Background backgrounds[] = {
{(const uint8_t *)background1, sizeof(background1)},
{(const uint8_t *)background2, sizeof(background2)},
{(const uint8_t *)background3, sizeof(background3)},
};

  • 構造体: 各背景画像は Background 構造体として保存され、以下を含みます:

    • data: PNG データへのポインタ。
    • size: PNG ファイルのサイズ。
  • 配列: backgrounds 配列はすべての背景画像を保存します。currentBackground 変数は現在表示されている背景を追跡します。

雪の粒子シミュレーション

  1. 粒子の初期化
void initParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].x = random(0, sprite.width());
particles[i].y = random(0, sprite.height());
particles[i].speed = random(3, 8);
}
}
  • ランダムな位置と速度で numParticles を初期化します。
  1. パーティクルの更新
void updateParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].speed += random(-1, 2); // Speed variation
particles[i].speed = constrain(particles[i].speed, 3, 8);
particles[i].y += particles[i].speed; // Move down
particles[i].x += random(-1, 2); // Wind effect
// Wrap-around logic
if (particles[i].y > sprite.height()) {
particles[i].y = 0;
particles[i].x = random(0, sprite.width());
particles[i].speed = random(3, 8);
}
if (particles[i].x < 0) particles[i].x = sprite.width();
if (particles[i].x > sprite.width()) particles[i].x = 0;
}
}
  • パーティクルの位置を以下で更新:
    • 落下効果:各パーティクルが下に移動。
    • 風効果:わずかな水平方向のドリフトを追加。
    • ラップアラウンド:パーティクルが底部を出ると上部にリセット。
  1. パーティクルのレンダリング:
void renderParticlesToSprite() {
for (int i = 0; i < numParticles; i++) {
sprite.fillCircle(particles[i].x, particles[i].y, 2, TFT_WHITE);
}
}
  • 各パーティクルを小さな白い円として描画します

PNG デコード

int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);
if (rc != PNG_SUCCESS) {
Serial.println("Failed to open PNG file!");
return;
}
png.decode(NULL, 0);
  • Loads and decodes the current background PNG using the png.openFLASH() function

Touch interaction

if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // Cycle backgrounds
delay(300); // Debounce
}
  • chsc6x_is_pressed() を使用してタッチイベントを検出し、currentBackground をインクリメントして背景画像を変更します

セットアップとループ

  • セットアップ:
void setup() {
Serial.begin(115200);
tft.begin();
tft.fillScreen(TFT_BLACK);
sprite.createSprite(240, 240); // Match display size
pinMode(TOUCH_INT, INPUT_PULLUP);
Wire.begin();
initParticles();
}
  • Initializes the display, touch input and snow particles

  • Main loop:

void loop() {
sprite.fillScreen(TFT_BLACK);
// Render background and snow
int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);
if (rc == PNG_SUCCESS) {
png.decode(NULL, 0);
updateParticles();
renderParticlesToSprite();
sprite.pushSprite(0, 0);
}
// Handle touch input
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds;
delay(300);
}
delay(10); // ~100 FPS
}
  • スプライトをクリアし、現在のフレーム(背景 + パーティクル)をレンダリングし、ユーザー入力をチェックします。

ダブルバッファリング

雪片のちらつきを減らし、アニメーションの滑らかさを向上させるために、ダブルバッファリングを使用します。

これにより、画面に表示する前にオフスクリーンバッファに描画することができます。

ここでのダブルバッファリング

このプロジェクトでは、TFT_eSPI ライブラリの TFT_eSprite クラスがダブルバッファリングを実装しています。

  1. スプライト作成
  • スプライト(オフスクリーンバッファ)は setup() 関数で作成されます:
sprite.createSprite(240, 240); // Match display size
  1. バッファの描画
  • すべての描画操作(背景レンダリングと雪の粒子アニメーション)はスプライト上で実行されます:
sprite.fillScreen(TFT_BLACK); // Clear the sprite
renderParticlesToSprite(); // Draw snow particles
  1. ディスプレイの更新
  • フレームがスプライト内で完全に描画された後、一度の操作でディスプレイにプッシュされます:
sprite.pushSprite(0, 0);
  • これにより、バッファの内容が瞬時に画面に転送されます。
  1. 再利用
  • スプライトは loop() の開始時にクリアすることで、すべてのフレームで再利用されます:
sprite.fillScreen(TFT_BLACK);

ダブルバッファリング使用の利点

  • スムーズな雪のアニメーション:落下する雪の粒子がちらつきなしにシームレスに更新されます。
  • 動的な背景切り替え:タッチによってトリガーされる背景変更が、目に見える遅延やアーティファクトなしに発生します。
  • 効率的なレンダリング:メモリ(RAM)での描画は、ディスプレイを直接行ごとに更新するよりも高速です。

プロジェクトの完全なコードは以下の通りです

/**
*
* To create the images as C arrays, visit:
* https://notisrac.github.io/FileToCArray/
*
*/
#include <PNGdec.h>
#include <TFT_eSPI.h>

#include "background1.h"
#include "background2.h"
#include "background3.h"

#define USE_TFT_ESPI_LIBRARY
#include "lv_xiao_round_screen.h"

// PNG decoder and TFT display instances
PNG png;
//TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft); // Off-screen buffer

#define MAX_IMAGE_WIDTH 240

// Backgrounds for the snow globe
struct Background {
const uint8_t *data;
size_t size;
};

// Define the backgrounds with explicit casting
const Background backgrounds[] = {
{(const uint8_t *)background1, sizeof(background1)},
{(const uint8_t *)background2, sizeof(background2)},
{(const uint8_t *)background3, sizeof(background3)},
};
const size_t numBackgrounds = sizeof(backgrounds) / sizeof(backgrounds[0]);

int currentBackground = 0; // Index of the current background

// Snow particle properties
const int numParticles = 100; // Number of snow particles
struct Particle {
int16_t x, y; // Position
int16_t speed; // Vertical speed
};
Particle particles[numParticles];

// Function to draw PNG to the sprite (callback for PNG decoder)
void pngDrawToSprite(PNGDRAW *pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
sprite.pushImage(0, pDraw->y, pDraw->iWidth, 1, lineBuffer);
}

// Initialize snow particles
void initParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].x = random(0, sprite.width());
particles[i].y = random(0, sprite.height());
particles[i].speed = random(3, 8); // Random speed for each snowflake
}
}

// Update snow particle positions
void updateParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].speed += random(-1, 2); // Random variation in speed
particles[i].speed = constrain(particles[i].speed, 3, 8);
particles[i].y += particles[i].speed;
particles[i].x += random(-1, 2); // Wind effect

// Wrap around screen
if (particles[i].y > sprite.height()) {
particles[i].y = 0;
particles[i].x = random(0, sprite.width());
particles[i].speed = random(3, 8);
}
if (particles[i].x < 0) particles[i].x = sprite.width();
if (particles[i].x > sprite.width()) particles[i].x = 0;
}
}

// Render snow particles to the sprite
void renderParticlesToSprite() {
for (int i = 0; i < numParticles; i++) {
sprite.fillCircle(particles[i].x, particles[i].y, 2, TFT_WHITE);
}
}

void setup() {
Serial.begin(115200);
Serial.println("\n\nUsing the PNGdec library with touch interaction");

// Initialize TFT
tft.begin();
tft.fillScreen(TFT_BLACK);
sprite.createSprite(240, 240); // Match display size

// Initialize touch interrupt pin
pinMode(TOUCH_INT, INPUT_PULLUP);
Wire.begin();

// Initialize particles
initParticles();

Serial.println("Setup complete.");
}

void loop() {
// Clear the sprite for the new frame
sprite.fillScreen(TFT_BLACK);

// Render the current background to the sprite
int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);


if (rc != PNG_SUCCESS) {
Serial.println("Failed to open PNG file!");
return;
}
png.decode(NULL, 0); // Decode and render background

// Update and render snow particles
updateParticles();
renderParticlesToSprite();

// Push the sprite to the display
sprite.pushSprite(0, 0);

// Check for touch input using chsc6x_is_pressed
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // Cycle through backgrounds
delay(300); // Debounce delay
}

delay(10); // ~100 FPS
}

今、あなた自身の写真を使って魔法のクリスマスボールを作ることができます。

✨ Contributor Project

Tech Support & Product Discussion

私たちの製品をお選びいただき、ありがとうございます!私たちは、お客様の製品体験が可能な限りスムーズになるよう、さまざまなサポートを提供しています。異なる好みやニーズに対応するため、複数のコミュニケーションチャンネルを提供しています。

Loading Comments...