Skip to main content

embassy_st7789v_sprite/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3
4//! # embassy-st7789v-sprite
5//!
6//! Moteur de sprites et d'animation style Piskel avec Framebuffer RAM
7//! pour l'écran ST7789V 240×320 via Embassy.
8//!
9//! ## Principe
10//!
11//! Tout le dessin s'effectue dans un buffer RAM de 153 600 octets
12//! (`SCREEN_W` × `SCREEN_H` pixels codés en RGB565, soit 2 octets/pixel).
13//! Toutes les opérations de dessin ([`SpriteEngine::clear`], [`SpriteEngine::draw_sprite`],
14//! [`SpriteEngine::draw_pixel`], [`SpriteEngine::fill_rect`]) sont **synchrones** et
15//! ne font que modifier le framebuffer en RAM, sans aucune communication SPI.
16//! Un seul appel [`SpriteEngine::flush`] (asynchrone) envoie ensuite la frame
17//! complète vers l'écran physique via SPI/DMA.
18//!
19//! Ce découpage permet de composer une scène complète (fond, sprites, HUD…)
20//! sans scintillement, puis de l'envoyer d'un bloc — une technique de
21//! double-bufferisation logicielle adaptée aux contraintes mémoire de l'embarqué.
22//!
23//! ## Exemple minimal
24//!
25//! ```rust,ignore
26//! use embassy_st7789v_sprite::{SpriteEngine, PiskelSprite, FB_SIZE};
27//! use embassy_st7789v::Color;
28//!
29//! static HERO_SPRITE: PiskelSprite = PiskelSprite {
30//!     width: 32,
31//!     height: 32,
32//!     frame_count: 4,
33//!     pixels: &HERO_PIXELS, // généré depuis un export Piskel (voir README)
34//! };
35//!
36//! // Le framebuffer doit vivre aussi longtemps que le moteur.
37//! static mut FRAMEBUFFER: [u16; FB_SIZE] = [0u16; FB_SIZE];
38//!
39//! # async fn demo(display: &mut embassy_st7789v::St7789v<impl embedded_hal_async::spi::SpiDevice, impl embedded_hal::digital::OutputPin>) {
40//! let framebuffer = unsafe { &mut *core::ptr::addr_of_mut!(FRAMEBUFFER) };
41//! let mut engine = SpriteEngine::new(display, framebuffer);
42//!
43//! engine.clear(Color(0x0000));                    // fond noir
44//! engine.draw_sprite(&HERO_SPRITE, 100, 200, 0);   // frame 0 du héros
45//! engine.flush().await.unwrap();                   // envoi à l'écran
46//! # }
47//! # static HERO_PIXELS: [u16; 32 * 32 * 4] = [0u16; 32 * 32 * 4];
48//! ```
49
50use embassy_st7789v::{Color, NoPin, St7789v};
51use embedded_hal::digital::OutputPin;
52use embedded_hal_async::spi::SpiDevice;
53
54/// Largeur de l'écran ST7789V en pixels.
55pub const SCREEN_W: usize = 240;
56
57/// Hauteur de l'écran ST7789V en pixels.
58pub const SCREEN_H: usize = 320;
59
60/// Nombre total de pixels du framebuffer (SCREEN_W × SCREEN_H).
61pub const FB_SIZE: usize = SCREEN_W * SCREEN_H; // 76 800 pixels → 153 600 octets
62
63/// Couleur-clé de transparence utilisée par [`SpriteEngine::draw_sprite`].
64///
65/// Tout pixel d'un [`PiskelSprite`] codé avec cette valeur RGB565
66/// (magenta pur, `0xF81F`) n'est **pas** copié dans le framebuffer :
67/// le fond existant (ciel, sol, autre sprite déjà dessiné…) reste visible.
68///
69/// Choisissez cette couleur comme fond transparent lors de l'export Piskel
70/// (ou en dessinant vos sprites), en évitant de vous en servir pour un détail
71/// réel du sprite.
72pub const TRANSPARENT_KEY: u16 = 0xF81F;
73
74// ─────────────────────────────────────────────────────────────────────────────
75// PiskelSprite
76// ─────────────────────────────────────────────────────────────────────────────
77
78/// Sprite statique ou planche d'animation exportée depuis Piskel.
79///
80/// Les données de pixels sont stockées en Flash (`&'static [u16]`).
81/// Chaque pixel est encodé en **RGB565** sur 16 bits.
82/// Le pixel [`TRANSPARENT_KEY`] est traité comme transparent par
83/// [`SpriteEngine::draw_sprite`].
84#[derive(Clone, Copy)]
85pub struct PiskelSprite {
86    /// Largeur d'une frame en pixels.
87    pub width: u16,
88    /// Hauteur d'une frame en pixels.
89    pub height: u16,
90    /// Nombre total de frames dans la planche.
91    pub frame_count: u16,
92    /// Données RGB565 de toutes les frames, en Flash.
93    pub pixels: &'static [u16],
94}
95
96// ─────────────────────────────────────────────────────────────────────────────
97// SpriteEngine
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// Moteur de rendu par framebuffer pour le ST7789V.
101pub struct SpriteEngine<'a, SPI, DC, RST = NoPin>
102where
103    SPI: SpiDevice,
104    DC: OutputPin,
105    RST: OutputPin,
106{
107    display: &'a mut St7789v<SPI, DC, RST>,
108    framebuffer: &'a mut [u16; FB_SIZE],
109}
110
111// ── Constructeur sans broche RST ─────────────────────────────────────────────
112
113impl<'a, SPI, DC> SpriteEngine<'a, SPI, DC, NoPin>
114where
115    SPI: SpiDevice,
116    DC: OutputPin,
117{
118    /// Crée un moteur pour un `St7789v` construit **sans** broche RST matérielle.
119    #[inline]
120    pub fn new_no_rst(
121        display: &'a mut St7789v<SPI, DC, NoPin>,
122        framebuffer: &'a mut [u16; FB_SIZE],
123    ) -> Self {
124        Self { display, framebuffer }
125    }
126}
127
128// ── Constructeur avec broche RST ─────────────────────────────────────────────
129
130impl<'a, SPI, DC, RST> SpriteEngine<'a, SPI, DC, RST>
131where
132    SPI: SpiDevice,
133    DC: OutputPin,
134    RST: OutputPin,
135{
136    /// Crée un moteur pour un `St7789v` avec broche RST matérielle.
137    #[inline]
138    pub fn new(
139        display: &'a mut St7789v<SPI, DC, RST>,
140        framebuffer: &'a mut [u16; FB_SIZE],
141    ) -> Self {
142        Self { display, framebuffer }
143    }
144
145    // ── Opérations sur le framebuffer (synchrones, RAM uniquement) ────────────
146
147    /// Remplit **tout** le framebuffer RAM avec `color` (aucune communication SPI).
148    #[inline]
149    pub fn clear(&mut self, color: Color) {
150        self.framebuffer.fill(color.0);
151    }
152
153    /// Dessine la frame `frame` d'un [`PiskelSprite`] dans le framebuffer RAM.
154    ///
155    /// # Transparence
156    ///
157    /// Tout pixel du sprite valant [`TRANSPARENT_KEY`] (magenta `0xF81F`)
158    /// est ignoré : le contenu déjà présent dans le framebuffer à cet
159    /// emplacement (fond, autre sprite…) reste inchangé.
160    ///
161    /// # Clipping automatique
162    ///
163    /// Les coordonnées `start_x` / `start_y` sont signées (`i16`).
164    /// Les pixels hors de la zone `[0, 240[ × [0, 320[` sont ignorés sans panique
165    /// ni débordement : le sprite peut donc sortir partiellement du bord.
166    ///
167    /// # Panics
168    ///
169    /// Aucun panic. Un index de frame invalide (`frame >= frame_count`) est
170    /// silencieusement ignoré.
171    pub fn draw_sprite(
172        &mut self,
173        sprite: &PiskelSprite,
174        start_x: i16,
175        start_y: i16,
176        frame: u16,
177    ) {
178        if frame >= sprite.frame_count {
179            return;
180        }
181
182        let w = sprite.width as i16;
183        let h = sprite.height as i16;
184        let frame_stride = (sprite.width as usize) * (sprite.height as usize);
185        let frame_offset = (frame as usize) * frame_stride;
186
187        for py in 0..h {
188            let screen_y = start_y + py;
189            if screen_y < 0 || screen_y >= SCREEN_H as i16 {
190                continue;
191            }
192
193            for px in 0..w {
194                let screen_x = start_x + px;
195                if screen_x < 0 || screen_x >= SCREEN_W as i16 {
196                    continue;
197                }
198
199                let sprite_pixel_idx =
200                    frame_offset + (py as usize) * (sprite.width as usize) + (px as usize);
201                let raw_color = sprite.pixels[sprite_pixel_idx];
202
203                // Pixel transparent : on n'écrit pas dans le framebuffer,
204                // ce qui laisse voir le fond déjà dessiné.
205                if raw_color == TRANSPARENT_KEY {
206                    continue;
207                }
208
209                let fb_idx = (screen_y as usize) * SCREEN_W + (screen_x as usize);
210                self.framebuffer[fb_idx] = raw_color;
211            }
212        }
213    }
214
215    /// Dessine un pixel isolé directement dans le framebuffer RAM.
216    #[inline]
217    pub fn draw_pixel(&mut self, x: i16, y: i16, color: Color) {
218        if x >= 0 && y >= 0 && (x as usize) < SCREEN_W && (y as usize) < SCREEN_H {
219           self.framebuffer[(y as usize) * SCREEN_W + (x as usize)] = color.0;
220        }
221    }
222
223    /// Remplit un rectangle dans le framebuffer RAM.
224    ///
225    /// `x0/y0` coin supérieur gauche, `x1/y1` coin inférieur droit (inclusifs).
226    /// Clippé automatiquement contre les bords de l'écran.
227    pub fn fill_rect(&mut self, x0: i16, y0: i16, x1: i16, y1: i16, color: Color) {
228        let raw = color.0;
229        let xa = x0.max(0) as usize;
230        let ya = y0.max(0) as usize;
231        let xb = (x1.min(SCREEN_W as i16 - 1)) as usize;
232        let yb = (y1.min(SCREEN_H as i16 - 1)) as usize;
233
234        if xa > xb || ya > yb {
235            return;
236        }
237
238        for row in ya..=yb {
239            let base = row * SCREEN_W;
240            self.framebuffer[base + xa..=base + xb].fill(raw);
241        }
242    }
243
244    // ── Envoi vers l'écran (asynchrone, SPI) ─────────────────────────────────
245
246    /// Envoie le contenu complet du framebuffer RAM vers l'écran physique.
247    pub async fn flush(&mut self) -> Result<(), SPI::Error> {
248        for y in 0..SCREEN_H {
249            let row_start = y * SCREEN_W;
250            for x in 0..SCREEN_W {
251                let raw_color = self.framebuffer[row_start + x];
252                let color = Color(raw_color);
253                self.display.draw_pixel(x as u16, y as u16, color).await?;
254            }
255        }
256        Ok(())
257    }
258}