Seeed Studio XIAO 圆形显示屏圣诞球

在本教程中,我将向您展示如何制作一个带有飘落雪花和变化背景图像的圣诞球。
该程序执行以下功能:
- 显示存储为 C 数组的背景图像。
- 模拟雪花粒子在图像上飘落,带有风效果。
- 检测触摸输入并循环切换一组背景图像。
- 使用双缓冲实现流畅的动画。
环境准备
硬件
对于这个项目,我们需要:
我使用 XIAO ESP32S3 是因为内存的原因。PNGDEC 需要一些内存来运行,大约 40KB。
软件准备
要使用圆形显示屏,请前往XIAO 圆形显示屏入门指南安装必要的库。
尝试一些示例,看看是否一切正常工作。
库文件
对于这个项目,我们将使用Seeed Studio XIAO 圆形显示屏附带的库。
按照教程XIAO 圆形显示屏入门指南中指定的方式安装所有库。 之后,您需要以下库:
- PNGdec 库。
- 更新 LVGL 库(或者不安装 Seeed Studio github 上的版本)
图像
我们的图像是存储在闪存数组中的 PNG 图像。它们使用 PNGdec 库显示。
所有图像必须是 PNG 格式
以下是我使用的图像 - 全部由 AI 生成



我们的背景图像需要准备好,以便 TFT_eSPI 可以显示它们,并且它们能很好地适配 XIAO 圆形显示屏。
准备图像
调整图像大小
我们的 XIAO 圆形显示屏分辨率为 240x240。我们需要调整图像大小。我将展示如何使用GIMP来完成。
- 打开图像
- 转到图像 > 缩放图像

- 将宽度和高度设置为 240。因为选择了保持比例(链条图标),一旦您更改宽度,高度也应该随之更改。

- 按缩放按钮。

- 保存图像(我将覆盖旧的图像)

创建闪存数组
注意: 这些说明在 TFT_eSPI Flash_PNG 示例中。
要创建闪存数组,请转到文件到 C 风格数组转换器
现在的步骤是:
- 使用浏览上传图像。上传图像后

- 我们需要设置一些选项
- 作为二进制处理

所有其他选项都变灰。

- 让我们将数据类型更改为char

- 按转换。这将把图像转换为数组。

- 您现在可以按保存为文件按钮保存您的图像并将其添加到您的 Arduino 草图中,或按复制到剪贴板按钮 如果您复制到剪贴板,您必须按 Arduino 编辑器右侧的 3 个点并选择新建选项卡

给它一个名称(通常是您的图像名称加上 .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 变量跟踪当前显示的背景。
雪花粒子模拟
- 粒子初始化
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。
- 粒子更新
void updateParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].speed += random(-1, 2); // 速度变化
particles[i].speed = constrain(particles[i].speed, 3, 8);
particles[i].y += particles[i].speed; // 向下移动
particles[i].x += random(-1, 2); // 风效果
// 环绕逻辑
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;
}
}
- 使用以下方式更新粒子位置:
- 下落效果:每个粒子向下移动。
- 风力效果:添加轻微的水平漂移。
- 环绕效果:粒子在从底部退出时重置到顶部。
- 渲染粒子:
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);
- 使用 png.openFLASH() 函数加载并解码当前背景 PNG
触摸交互
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // 循环切换背景
delay(300); // 防抖
}
- 使用 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();
}
-
初始化显示屏、触摸输入和雪花粒子
-
主循环:
void loop() {
sprite.fillScreen(TFT_BLACK);
// 渲染背景和雪花
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);
}
// 处理触摸输入
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds;
delay(300);
}
delay(10); // ~100 FPS
}
- 清除精灵,渲染当前帧(背景 + 粒子),并检查用户输入。
双缓冲
为了减少雪花的闪烁并改善动画的流畅性,我们使用双缓冲。
这允许我们在屏幕外缓冲区中绘制,然后再显示到屏幕上。
这里的双缓冲
在这个项目中,TFT_eSPI 库的 TFT_eSprite 类实现了双缓冲。
- 精灵创建
- 精灵(屏幕外缓冲区)在 setup() 函数中创建:
sprite.createSprite(240, 240); // 匹配显示尺寸
- 绘制缓冲区
- 所有绘制操作(背景渲染和雪花粒子动画)都在精灵上完成:
sprite.fillScreen(TFT_BLACK); // 清除精灵
renderParticlesToSprite(); // 绘制雪花粒子
- 更新显示
- 在精灵中完全绘制帧后,通过一次操作将其推送到显示屏:
sprite.pushSprite(0, 0);
- 这会立即将缓冲区的内容传输到屏幕上。
- 重用
- 通过在 loop() 开始时清除精灵来为每一帧重用精灵:
sprite.fillScreen(TFT_BLACK);
使用双缓冲的优势
- 流畅的雪花动画:下落的雪花粒子更新无缝,没有闪烁。
- 动态背景切换:触摸触发的背景变化没有可见的延迟或伪影。
- 高效渲染:在内存(RAM)中绘制比直接逐行更新显示更快。
以下是项目的完整代码:
/**
*
* 要将图像创建为C数组,请访问:
* 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解码器和TFT显示实例
PNG png;
//TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft); // 离屏缓冲区
#define MAX_IMAGE_WIDTH 240
// 雪花球的背景
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)},
};
const size_t numBackgrounds = sizeof(backgrounds) / sizeof(backgrounds[0]);
int currentBackground = 0; // 当前背景的索引
// 雪花粒子属性
const int numParticles = 100; // 雪花粒子数量
struct Particle {
int16_t x, y; // 位置
int16_t speed; // 垂直速度
};
Particle particles[numParticles];
// 将PNG绘制到精灵的函数(PNG解码器的回调函数)
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);
}
// 初始化雪花粒子
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); // 每个雪花的随机速度
}
}
// 更新雪花粒子位置
void updateParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].speed += random(-1, 2); // 速度的随机变化
particles[i].speed = constrain(particles[i].speed, 3, 8);
particles[i].y += particles[i].speed;
particles[i].x += random(-1, 2); // 风效果
// 屏幕环绕
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;
}
}
// 将雪花粒子渲染到精灵
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\n使用PNGdec库进行触摸交互");
// 初始化TFT
tft.begin();
tft.fillScreen(TFT_BLACK);
sprite.createSprite(240, 240); // 匹配显示尺寸
// 初始化触摸中断引脚
pinMode(TOUCH_INT, INPUT_PULLUP);
Wire.begin();
// 初始化粒子
initParticles();
Serial.println("设置完成。");
}
void loop() {
// 为新帧清除精灵
sprite.fillScreen(TFT_BLACK);
// 将当前背景渲染到精灵
int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);
if (rc != PNG_SUCCESS) {
Serial.println("无法打开PNG文件!");
return;
}
png.decode(NULL, 0); // 解码并渲染背景
// 更新并渲染雪花粒子
updateParticles();
renderParticlesToSprite();
// 将精灵推送到显示器
sprite.pushSprite(0, 0);
// 使用chsc6x_is_pressed检查触摸输入
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // 循环切换背景
delay(300); // 防抖延迟
}
delay(10); // ~100 FPS
}
现在您可以使用自己的图片来创建一个神奇的圣诞球。
✨ 贡献者项目
- 此项目由 Seeed Studio 贡献者项目支持。
- 感谢 Bruno Santos,您的作品将被展示。
技术支持与产品讨论
感谢您选择我们的产品!我们在这里为您提供不同的支持,以确保您使用我们产品的体验尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。