Pular para o conteúdo principal

Bola de Natal com Seeed Studio Round Display para XIAO

Neste tutorial vou mostrar como você pode criar uma bola de Natal com neve caindo e imagens de fundo que mudam.

O programa executa o seguinte:

  • Exibe uma imagem de fundo armazenada como um array em C.
  • Simula partículas de neve caindo sobre a imagem, com um efeito de vento.
  • Detecta o toque e percorre um conjunto de imagens de fundo.
  • Usa double-buffering para animações suaves.

Preparação do Ambiente

Hardware

Para o projeto, vamos precisar de:

Estou usando o XIAO ESP32S3 por causa da memória. A PNGDEC precisa de um pouco de memória para rodar ~40 kbytes.

Preparação de Software

Para usar o Round Display, vá para Getting Started with Round Display for XIAO para instalar as bibliotecas necessárias.

Experimente alguns dos exemplos para ver se tudo está funcionando bem.

Bibliotecas

Para este projeto, vamos usar as bibliotecas que vêm junto com o Seeed Studio Round Display For XIAO

Instale todas as bibliotecas como especificado no tutorial Getting Started with Round Display for XIAO. Depois disso, você precisa do seguinte:

  • Biblioteca PNGdec.
  • Atualizar a biblioteca LVGL (ou não instalar a do github da Seeed Studio)

Imagens

Nossas imagens são imagens PNG armazenadas em Flash Arrays. Elas são exibidas usando a biblioteca PNGdec.

Todas as imagens devem ser PNG

Aqui estão as imagens que eu usei - todas são geradas por IA

Nossas imagens de fundo precisam ser preparadas para que a TFT_eSPI possa exibi-las e elas caibam bem no Round Display for XIAO.

Preparar imagens

Redimensionar Imagens

Nosso Round Display for XIAO tem resolução de 240x240. Precisamos redimensionar as imagens. Vou mostrar como fazer isso usando o GIMP

  1. Abra a imagem
  2. Vá em Image > Scale Image
  1. Defina Largura e Altura para 240. Como o Keep Ratio está selecionado (o cadeado), quando você alterar a width, a height também deve mudar.
  1. Pressione o botão Scale.
  1. Salve a imagem (vou sobrescrever a antiga)

Criar os Flash Arrays

NOTA: Estas instruções estão dentro do exemplo TFT_eSPI Flash_PNG.

Para criar o flash array, vá para File to C style array converter

Os passos agora são:

  1. Envie a imagem usando Browse. Após enviar a imagem
  1. Precisamos definir algumas opções
  • Treat as binary

Todas as outras opções ficam em cinza.

  1. Vamos mudar o Data type para char
  1. Pressione convert. Isso vai converter a imagem em um array.
  1. Agora você pode pressionar o botão Save as file para salvar sua imagem e adicioná-la ao seu Sketch Arduino ou pressionar o botão Copy to clipboard Se você usar Copy to clipboard, terá que pressionar os 3 pontos no lado direito do editor Arduino e escolher New Tab

Dê um nome (geralmente o nome da sua imagem com extensão .h)

Você vai terminar com todas as suas imagens como arquivos .h.

Código

Aqui está o código para a bola de Natal. Uma pequena explicação das principais funções do código. O código também inclui alguns comentários.

Headers e bibliotecas

Começamos incluindo algumas bibliotecas.

#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"

Lembre-se de que você precisa ter as bibliotecas da Seeed Studio instaladas.

Imagens de fundo

Aqui estão as funções para gerenciar as imagens de fundo

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)},
};

  • Struct: Cada imagem de fundo é armazenada como uma struct Background contendo:

    • data: Ponteiro para os dados PNG.
    • size: Tamanho do arquivo PNG.
  • Array: O array backgrounds armazena todas as imagens de fundo. A variável currentBackground rastreia o fundo exibido atualmente.

Simulação das partículas de neve

  1. Inicialização das 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 com posições e velocidades aleatórias.
  1. Atualização das 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;
}
}
  • Atualiza as posições das partículas com:
    • Efeito de Queda: Cada partícula se move para baixo.
    • Efeito de Vento: Adiciona um leve desvio horizontal.
    • Wrap Around: As partículas voltam para o topo quando saem pela parte de baixo.
  1. Renderização das 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 um pequeno círculo branco

Decodificação de 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);
  • Carrega e decodifica o PNG de fundo atual usando a função png.openFLASH()

Interação por toque

if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // Cycle backgrounds
delay(300); // Debounce
}
  • Detecta um evento de toque usando chsc6x_is_pressed() e altera a imagem de fundo incrementando currentBackground

Setup e loop

  • Setup:
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 o display, o toque e as partículas de neve

  • Loop 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
}
  • Limpa o sprite, renderiza o quadro atual (plano de fundo + partículas) e verifica a entrada do usuário.

Duplo buffer

Para reduzir o tremeluzir e melhorar a suavidade da animação dos flocos de neve, usamos duplo buffer.

Isso nos permite desenhar em um buffer fora da tela antes de exibi-lo na tela.

Duplo buffer aqui

Neste projeto, a classe TFT_eSprite da biblioteca TFT_eSPI implementa o duplo buffer.

  1. Criação do sprite
  • O sprite (buffer fora da tela) é criado na função setup():
sprite.createSprite(240, 240); // Match display size
  1. Desenho do buffer
  • Todas as operações de desenho (renderização do plano de fundo e animação das partículas de neve) são feitas no sprite:
sprite.fillScreen(TFT_BLACK); // Clear the sprite
renderParticlesToSprite(); // Draw snow particles
  1. Atualização do display
  • Depois que o quadro é totalmente desenhado no sprite, ele é enviado ao display em uma única operação:
sprite.pushSprite(0, 0);
  • Isso transfere instantaneamente o conteúdo do buffer para a tela.
  1. Reutilização
  • O sprite é reutilizado para cada quadro, sendo limpo no início do loop():
sprite.fillScreen(TFT_BLACK);

Vantagens de usar duplo buffer

  • Animação suave da neve: As partículas de neve em queda são atualizadas de forma contínua, sem tremeluzir.
  • Troca dinâmica de plano de fundo: As mudanças de plano de fundo acionadas por toque acontecem sem atrasos visíveis ou artefatos.
  • Renderização eficiente: Desenhar na memória (RAM) é mais rápido do que atualizar o display diretamente linha por linha.

Aqui está o código completo do projeto:

/**
*
* 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
}

Agora você pode usar suas próprias imagens para criar uma Bola de Natal mágica.

✨ Projeto de Colaborador

Suporte Técnico & Discussão de Produto

Obrigado por escolher nossos produtos! Estamos aqui para oferecer diferentes formas de suporte para garantir que sua experiência com nossos produtos seja a mais tranquila possível. Oferecemos vários canais de comunicação para atender a diferentes preferências e necessidades.

Loading Comments...