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}