Bola de Navidad con Seeed Studio Round Display para XIAO

En este tutorial te voy a mostrar cómo puedes crear una bola de Navidad con nieve cayendo e imágenes de fondo que cambian.
El programa realiza lo siguiente:
- Muestra una imagen de fondo almacenada como un array de C.
- Simula partículas de nieve cayendo sobre la imagen, con un efecto de viento.
- Detecta entrada táctil y recorre un conjunto de imágenes de fondo.
- Usa doble buffer para animaciones suaves.
Preparación del Entorno
Hardware
Para el proyecto, vamos a necesitar:
Estoy usando el XIAO ESP32S3 debido a la memoria. El PNGDEC requiere un poco de memoria para ejecutarse ~40kbytes.
Preparación del Software
Para usar el Round Display, dirígete a Comenzando con Round Display para XIAO para instalar las librerías necesarias.
Prueba algunos de los ejemplos para ver si todo funciona bien.
Librerías
Para este proyecto, vamos a usar las librerías que vienen incluidas con el Seeed Studio Round Display Para XIAO
Instala todas las librerías como se especifica en el tutorial Comenzando con Round Display para XIAO. Después de eso, necesitas lo siguiente:
- Librería PNGdec.
- Actualizar librería LVGL (o no instalar la del github de Seeed Studio)
Imágenes
Nuestras imágenes son imágenes PNG almacenadas en Arrays Flash. Se muestran usando la librería PNGdec.
Todas las imágenes deben ser PNG
Aquí están las imágenes que he usado - todas son generadas por IA



Nuestras imágenes de fondo necesitan ser preparadas para que TFT_eSPI pueda mostrarlas y encajen bien en el Round Display para XIAO.
Preparar imágenes
Redimensionar Imágenes
Nuestro Round Display para XIAO tiene una resolución de 240x240. Necesitamos redimensionar las imágenes. Voy a mostrar cómo hacerlo usando GIMP
- Abre la imagen
- Ve a Imagen > Escalar Imagen

- Establece Ancho y Alto a 240. Porque Mantener Proporción está seleccionado (la cadena), una vez que cambies el ancho, la altura también debería cambiar.

- Presiona el botón Escalar.

- Guarda la imagen (voy a sobrescribir la anterior)

Crear los Arrays Flash
NOTA: Estas instrucciones están dentro del ejemplo Flash_PNG de TFT_eSPI.
Para crear el array flash, ve a Convertidor de archivo a array estilo C
Los pasos ahora son:
- Sube la imagen usando Examinar. Después de subir la imagen

- Necesitamos establecer algunas opciones
- Tratar como binario

Todas las otras opciones se ponen en gris.

- Cambiemos el Tipo de datos a char

- Presiona convertir. Esto convertirá la imagen a un array.

- Ahora puedes presionar el botón Guardar como archivo para guardar tu imagen y añadirla a tu Sketch de Arduino o presionar el botón Copiar al portapapeles Si Copias al portapapeles, tendrás que presionar los 3 puntos en el lado derecho del editor de Arduino y elegir Nueva Pestaña

Dale un nombre (generalmente el nombre de tu imagen con extensión .h)

Terminarás con todas tus imágenes como archivos .h.

Código
Aquí está el código para la bola de Navidad. Una pequeña explicación de las funciones principales del código. El código también incluye algunos comentarios.
Encabezados y librerías
Comenzamos incluyendo algunas librerías.
#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"
Recuerda que necesitas tener las librerías de Seeed Studio instaladas.
Imágenes de fondo
Aquí están las funciones para gestionar las imágenes de fondo
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)},
};
-
Estructura: Cada imagen de fondo se almacena como una estructura Background que contiene:
- data: Puntero a los datos PNG.
- size: Tamaño del archivo PNG.
-
Array: El array backgrounds almacena todas las imágenes de fondo. La variable currentBackground rastrea el fondo actualmente mostrado.
Simulación de partículas de nieve
- Inicialización de partículas
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);
}
}
- Inicializa numParticles con posiciones y velocidades aleatorias.
- Actualizaciones de partículas
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;
}
}
- Actualiza las posiciones de las partículas con:
- Efecto de Caída: Cada partícula se mueve hacia abajo.
- Efecto de Viento: Añade una ligera deriva horizontal.
- Reinicio Circular: Las partículas se reinician en la parte superior cuando salen por la parte inferior.
- Renderizado de partículas:
void renderParticlesToSprite() {
for (int i = 0; i < numParticles; i++) {
sprite.fillCircle(particles[i].x, particles[i].y, 2, TFT_WHITE);
}
}
- Renderiza cada partícula como un pequeño círculo blanco
Decodificación 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
}
- Detecta un evento táctil usando chsc6x_is_pressed() y cambia la imagen de fondo incrementando currentBackground
Configuración y bucle
- Configuración:
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();
}
-
Inicializa la pantalla, entrada táctil y partículas de nieve
-
Bucle principal:
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
}
- Limpia el sprite, renderiza el frame actual (fondo + partículas), y verifica la entrada del usuario.
Doble buffer
Para reducir el parpadeo y mejorar la suavidad de la animación de los copos de nieve, utilizamos doble buffer.
Esto nos permite dibujar en un buffer fuera de pantalla antes de mostrarlo en la pantalla.
Doble buffer aquí
En este proyecto, la clase TFT_eSprite de la biblioteca TFT_eSPI implementa el doble buffer.
- Creación del sprite
- El sprite (buffer fuera de pantalla) se crea en la función setup():
sprite.createSprite(240, 240); // Match display size
- Dibujando el búfer
- Todas las operaciones de dibujo (renderizado de fondo y animación de partículas de nieve) se realizan en el sprite:
sprite.fillScreen(TFT_BLACK); // Clear the sprite
renderParticlesToSprite(); // Draw snow particles
- Actualizando la pantalla
- Después de que el marco esté completamente dibujado en el sprite, se envía a la pantalla en una sola operación:
sprite.pushSprite(0, 0);
- Esto transfiere el contenido del búfer a la pantalla instantáneamente.
- Reutilización
- El sprite se reutiliza para cada fotograma limpiándolo al inicio del loop():
sprite.fillScreen(TFT_BLACK);
Ventajas de Usar Doble Buffer
- Animación de Nieve Suave: Las partículas de nieve que caen se actualizan sin problemas y sin parpadeo.
- Cambio Dinámico de Fondo: Los cambios de fondo activados por toque ocurren sin retrasos visibles o artefactos.
- Renderizado Eficiente: Dibujar en memoria (RAM) es más rápido que actualizar directamente la pantalla línea por línea.
Aquí está el código completo para el proyecto:
/**
*
* 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
}
Ahora puedes usar tus propias imágenes para crear una Bola de Navidad mágica.
✨ Proyecto de Colaborador
- Este proyecto está respaldado por el Proyecto de Colaborador de Seeed Studio.
- Gracias Bruno Santos y tu trabajo será exhibido.
Soporte Técnico y Discusión de Productos
¡Gracias por elegir nuestros productos! Estamos aquí para brindarte diferentes tipos de soporte para asegurar que tu experiencia con nuestros productos sea lo más fluida posible. Ofrecemos varios canales de comunicación para satisfacer diferentes preferencias y necesidades.