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...