Skip to main content

embassy_ssd1306_graphics/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! # embassy-ssd1306-graphics
4//!
5//! Couche graphique 2D `no_std` pour écrans OLED SSD1306 (128×64),
6//! construite au-dessus de `embassy-ssd1306`.
7//!
8//! ## Rôle exact de ce crate
9//!
10//! Le driver `embassy-ssd1306` fournit déjà :
11//! - `draw_pixel()`, `draw_hline()`, `draw_vline()`
12//! - `draw_rect()`, `draw_filled_rect()`
13//! - `draw_char()`, `draw_str()`, `draw_i16()`
14//! - `draw_bitmap()`
15//! - `clear()`, `fill()`, `flush()`
16//!
17//! Ce crate **ne duplique rien**. Il ajoute uniquement les primitives
18//! que le driver ne propose pas :
19//!
20//! | Fonction          | Algorithme              |
21//! |-------------------|-------------------------|
22//! | [`line()`]          | Bresenham integer-only  |
23//! | [`circle`]        | Midpoint integer-only   |
24//! | [`fill_circle`]   | Midpoint + hlines       |
25//! | [`triangle`]      | 3 appels à `line()`     |
26//! | [`ellipse`]       | Midpoint généralisé     |
27//! | [`bezier_quad`]   | De Casteljau integer-only |
28//! | [`fill_triangle`] | Scanline integer-only   |
29//! ## Architecture
30//!
31//! ```text
32//! ┌──────────────────────────────────────┐
33//! │          Votre application           │
34//! │  line() / circle() / triangle() …   │
35//! │  oled.draw_str() / oled.draw_i16()  │  ← driver direct pour le texte
36//! └──────────┬───────────────────────────┘
37//!            │ &mut Graphics       │ &mut Ssd1306
38//! ┌──────────▼───────────┐         │
39//! │  Graphics (ce crate) │         │
40//! │  clipping · pixel()  │         │
41//! └──────────┬───────────┘         │
42//!            └─────────────────────┘
43//!                    │ draw_pixel()
44//! ┌──────────────────▼───────────────────┐
45//! │       embassy-ssd1306 (driver)       │
46//! │  framebuffer · I2C · flush()         │
47//! └──────────────────────────────────────┘
48//! ```
49//!
50//! ## Patron de borrow
51//!
52//! `Graphics` tient un `&mut Ssd1306` pour toute sa durée de vie.
53//! Pour appeler `oled.flush()`, `oled.clear()` ou `oled.draw_str()`,
54//! `gfx` doit être sorti de portée au préalable.
55//!
56//! ```rust,no_run
57//! loop {
58//!     oled.clear();
59//!     {
60//!         let mut gfx = Graphics::new(&mut oled);
61//!         line(&mut gfx, 0, 0, 127, 63, true);
62//!         circle(&mut gfx, 64, 32, 20, true);
63//!     } // ← borrow libéré
64//!     oled.draw_str(40, 3, b"RPi2350");
65//!     oled.flush().await.unwrap();
66//! }
67//! ```
68
69use embassy_ssd1306::Ssd1306;
70use embedded_hal_async::i2c::I2c;
71
72// ─────────────────────────────────────────────────────────────────────────────
73// Contexte graphique
74// ─────────────────────────────────────────────────────────────────────────────
75
76/// Contexte graphique.
77///
78/// Wraps minimalement un `&mut Ssd1306<I>` pour :
79/// - centraliser le **clipping** des coordonnées
80/// - fournir un `pixel()` signé (`i32`) aux algorithmes Bresenham / midpoint
81///
82/// Le driver reste propriétaire du framebuffer et du bus I2C.
83pub struct Graphics<'a, I: I2c> {
84    display: &'a mut Ssd1306<I>,
85}
86
87impl<'a, I: I2c> Graphics<'a, I> {
88    /// Crée un contexte graphique pour un écran 128×64.
89    #[inline]
90    pub fn new(display: &'a mut Ssd1306<I>) -> Self {
91        Self { display }
92    }
93
94    /// Dessine un pixel avec clipping automatique.
95    ///
96    /// Les coordonnées négatives ou hors de `[0, 128[` × `[0, 64[`
97    /// sont silencieusement ignorées aucun panic, aucun wrapping.
98    ///
99    /// Le driver gère lui-même un second clipping sur `u8` ;
100    /// ce niveau-ci permet aux algorithmes de travailler en `i32`
101    /// sans conversions coûteuses.
102    #[inline(always)]
103    pub fn pixel(&mut self, x: i32, y: i32, on: bool) {
104        if x >= 0 && y >= 0 && x < 128 && y < 64 {
105            self.display.draw_pixel(x as u8, y as u8, on);
106        }
107    }
108}
109
110// ─────────────────────────────────────────────────────────────────────────────
111// Ligne Bresenham
112// ─────────────────────────────────────────────────────────────────────────────
113
114/// Trace une ligne entre `(x0, y0)` et `(x1, y1)`.
115///
116/// **Algorithme :** Bresenham integer-only.  
117/// Zéro division flottante, zéro multiplication,safe sur tout MCU sans FPU.
118///
119/// # Exemple
120///
121/// ```rust,no_run
122/// line(&mut gfx, 0, 0, 127, 63, true);  // diagonale complète
123/// line(&mut gfx, 0, 0, 127, 63, false); // efface la diagonale
124/// ```
125pub fn line<I: I2c>(
126    gfx: &mut Graphics<'_, I>,
127    mut x0: i32,
128    mut y0: i32,
129    x1: i32,
130    y1: i32,
131    on: bool,
132) {
133    let dx = (x1 - x0).abs();
134    let sx = if x0 < x1 { 1 } else { -1 };
135    let dy = -(y1 - y0).abs();
136    let sy = if y0 < y1 { 1 } else { -1 };
137    let mut err = dx + dy;
138
139    loop {
140        gfx.pixel(x0, y0, on);
141        if x0 == x1 && y0 == y1 {
142            break;
143        }
144        let e2 = 2 * err;
145        if e2 >= dy {
146            err += dy;
147            x0 += sx;
148        }
149        if e2 <= dx {
150            err += dx;
151            y0 += sy;
152        }
153    }
154}
155
156// ─────────────────────────────────────────────────────────────────────────────
157// Cercle midpoint
158// ─────────────────────────────────────────────────────────────────────────────
159
160/// Trace le **contour** d'un cercle.
161///
162/// **Algorithme :** midpoint circle integer-only.  
163/// Exploite la symétrie 8-octants : chaque itération dessine 8 pixels
164/// symétriques, ce qui minimise le nombre d'appels à `pixel()`.
165///
166/// # Paramètres
167///
168/// - `(cx, cy)` : centre
169/// - `r` : rayon en pixels
170///
171/// # Exemple
172///
173/// ```rust,no_run
174/// circle(&mut gfx, 64, 32, 20, true);
175/// ```
176pub fn circle<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, r: i32, on: bool) {
177    if r <= 0 {
178        gfx.pixel(cx, cy, on);
179        return;
180    }
181    let mut x = r;
182    let mut y = 0;
183    let mut err = 0;
184
185    while x >= y {
186        gfx.pixel(cx + x, cy + y, on);
187        gfx.pixel(cx + y, cy + x, on);
188        gfx.pixel(cx - y, cy + x, on);
189        gfx.pixel(cx - x, cy + y, on);
190        gfx.pixel(cx - x, cy - y, on);
191        gfx.pixel(cx - y, cy - x, on);
192        gfx.pixel(cx + y, cy - x, on);
193        gfx.pixel(cx + x, cy - y, on);
194
195        y += 1;
196        if err <= 0 {
197            err += 2 * y + 1;
198        } else {
199            x -= 1;
200            err += 2 * (y - x) + 1;
201        }
202    }
203}
204
205/// **Remplit** un cercle (disque plein).
206///
207/// Utilise le même algorithme midpoint, mais dessine des lignes
208/// horizontales entre les points symétriques à chaque rangée.
209/// Beaucoup plus rapide que d'appeler `circle()` en spirale.
210///
211/// # Exemple
212///
213/// ```rust,no_run
214/// fill_circle(&mut gfx, 64, 32, 15, true);
215/// ```
216pub fn fill_circle<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, r: i32, on: bool) {
217    if r <= 0 {
218        gfx.pixel(cx, cy, on);
219        return;
220    }
221    let mut x = r;
222    let mut y = 0;
223    let mut err = 0;
224
225    while x >= y {
226        // Lignes horizontales symétriques (haut/bas, gauche/droite)
227        for px in (cx - x)..=(cx + x) {
228            gfx.pixel(px, cy + y, on);
229            gfx.pixel(px, cy - y, on);
230        }
231        for px in (cx - y)..=(cx + y) {
232            gfx.pixel(px, cy + x, on);
233            gfx.pixel(px, cy - x, on);
234        }
235
236        y += 1;
237        if err <= 0 {
238            err += 2 * y + 1;
239        } else {
240            x -= 1;
241            err += 2 * (y - x) + 1;
242        }
243    }
244}
245
246// ─────────────────────────────────────────────────────────────────────────────
247// Triangle
248// ─────────────────────────────────────────────────────────────────────────────
249
250/// Trace le **contour** d'un triangle défini par trois sommets.
251///
252/// Implémenté comme trois appels à [`line()`], aucune logique propre.
253///
254/// # Exemple
255///
256/// ```rust,no_run
257/// triangle(&mut gfx, 64, 4, 20, 59, 108, 59, true);
258/// ```
259#[inline]
260pub fn triangle<I: I2c>(
261    gfx: &mut Graphics<'_, I>,
262    x0: i32, y0: i32,
263    x1: i32, y1: i32,
264    x2: i32, y2: i32,
265    on: bool,
266) {
267    line(gfx, x0, y0, x1, y1, on);
268    line(gfx, x1, y1, x2, y2, on);
269    line(gfx, x2, y2, x0, y0, on);
270}
271
272
273
274
275// ─────────────────────────────────────────────────────────────────────────────
276// Ellipse midpoint généralisé
277// ─────────────────────────────────────────────────────────────────────────────
278
279/// Trace le **contour** d'une ellipse.
280///
281/// **Algorithme :** midpoint ellipse integer-only (Bresenham généralisé).
282/// Deux phases : région 1 (pente < -1) puis région 2 (pente > -1).
283///
284/// # Paramètres
285///
286/// - `(cx, cy)` : centre
287/// - `rx` : demi-axe horizontal
288/// - `ry` : demi-axe vertical
289///
290/// # Exemple
291///
292/// ```rust,no_run
293/// ellipse(&mut gfx, 64, 32, 40, 20, true); // ellipse large
294/// ellipse(&mut gfx, 64, 32, 10, 10, true); // cercle (rx == ry)
295/// ```
296pub fn ellipse<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, rx: i32, ry: i32, on: bool) {
297    if rx <= 0 || ry <= 0 {
298        gfx.pixel(cx, cy, on);
299        return;
300    }
301
302    let rx2 = rx * rx;
303    let ry2 = ry * ry;
304
305    let mut x = 0i32;
306    let mut y = ry;
307
308    // Région 1
309    let mut d1 = ry2 - rx2 * ry + rx2 / 4;
310    let mut dx = 2 * ry2 * x;
311    let mut dy = 2 * rx2 * y;
312
313    while dx < dy {
314        gfx.pixel(cx + x, cy + y, on);
315        gfx.pixel(cx - x, cy + y, on);
316        gfx.pixel(cx + x, cy - y, on);
317        gfx.pixel(cx - x, cy - y, on);
318
319        x += 1;
320        dx += 2 * ry2;
321        if d1 < 0 {
322            d1 += dx + ry2;
323        } else {
324            y -= 1;
325            dy -= 2 * rx2;
326            d1 += dx - dy + ry2;
327        }
328    }
329
330    // Région 2
331    let mut d2 = ry2 * (x * x + x) + rx2 * (y * y - 2 * y + 1) - rx2 * ry2 + rx2;
332
333    while y >= 0 {
334        gfx.pixel(cx + x, cy + y, on);
335        gfx.pixel(cx - x, cy + y, on);
336        gfx.pixel(cx + x, cy - y, on);
337        gfx.pixel(cx - x, cy - y, on);
338
339        y -= 1;
340        dy -= 2 * rx2;
341        if d2 > 0 {
342            d2 += rx2 - dy;
343        } else {
344            x += 1;
345            dx += 2 * ry2;
346            d2 += dx - dy + rx2;
347        }
348    }
349}
350
351// ─────────────────────────────────────────────────────────────────────────────
352// Courbe de Bézier quadratique De Casteljau
353// ─────────────────────────────────────────────────────────────────────────────
354
355/// Trace une **courbe de Bézier quadratique** (3 points de contrôle).
356///
357/// **Algorithme :** De Casteljau integer-only avec subdivision fixe.
358/// `steps` contrôle la finesse du tracé (16–32 suffisent pour 128×64).
359///
360/// Les interpolations sont faites en entiers avec précision ×1024
361/// pour éviter tout flottant.
362///
363/// # Paramètres
364///
365/// - `(x0, y0)` : point de départ
366/// - `(x1, y1)` : point de contrôle
367/// - `(x2, y2)` : point d'arrivée
368/// - `steps` : nombre de segments (recommandé : 16 à 32)
369///
370/// # Exemple
371///
372/// ```rust,no_run
373/// bezier_quad(&mut gfx, 10, 50, 64, 5, 118, 50, 24, true); // arche
374/// ```
375pub fn bezier_quad<I: I2c>(
376    gfx: &mut Graphics<'_, I>,
377    x0: i32, y0: i32,
378    x1: i32, y1: i32,
379    x2: i32, y2: i32,
380    steps: i32,
381    on: bool,
382) {
383    if steps <= 0 {
384        return;
385    }
386
387    let mut px = x0;
388    let mut py = y0;
389
390    for i in 1..=steps {
391        // t = i / steps en virgule fixe ×1024
392        let t  = (i * 1024) / steps;         // t  ∈ [0, 1024]
393        let t1 = 1024 - t;                   // 1-t
394
395        // B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2  (tout ×1024²)
396        let nx = (t1 * t1 * x0 + 2 * t1 * t * x1 + t * t * x2) / (1024 * 1024);
397        let ny = (t1 * t1 * y0 + 2 * t1 * t * y1 + t * t * y2) / (1024 * 1024);
398
399        line(gfx, px, py, nx, ny, on);
400        px = nx;
401        py = ny;
402    }
403}
404
405// ─────────────────────────────────────────────────────────────────────────────
406// Triangle plein  scanline
407// ─────────────────────────────────────────────────────────────────────────────
408
409/// **Remplit** un triangle défini par trois sommets.
410///
411/// **Algorithme :** scanline — tri des sommets par Y, puis
412/// interpolation linéaire integer-only des bords gauche/droit
413/// à chaque rangée horizontale.
414///
415/// # Exemple
416///
417/// ```rust,no_run
418/// fill_triangle(&mut gfx, 64, 4, 20, 59, 108, 59, true);
419/// ```
420pub fn fill_triangle<I: I2c>(
421    gfx: &mut Graphics<'_, I>,
422    x0: i32, mut y0: i32,
423    x1: i32, mut y1: i32,
424    x2: i32, mut y2: i32,
425    on: bool,
426) {
427    // Tri des sommets par Y croissant (bubble sort sur 3 éléments)
428    let (mut x0, mut x1, mut x2) = (x0, x1, x2);
429    if y0 > y1 { core::mem::swap(&mut y0, &mut y1); core::mem::swap(&mut x0, &mut x1); }
430    if y1 > y2 { core::mem::swap(&mut y1, &mut y2); core::mem::swap(&mut x1, &mut x2); }
431    if y0 > y1 { core::mem::swap(&mut y0, &mut y1); core::mem::swap(&mut x0, &mut x1); }
432
433    let total_h = y2 - y0;
434    if total_h == 0 {
435        // Triangle dégénéré tracer une seule ligne
436        let xmin = x0.min(x1).min(x2);
437        let xmax = x0.max(x1).max(x2);
438        for x in xmin..=xmax {
439            gfx.pixel(x, y0, on);
440        }
441        return;
442    }
443
444    let upper_h = y1 - y0;
445    let lower_h = y2 - y1;
446
447    // Moitié supérieure : y0 → y1
448    for y in y0..=y1 {
449        let dy = y - y0;
450        // Interpolation integer-only ×total_h pour éviter la division
451        let xa = x0 + (x2 - x0) * dy / total_h;
452        let xb = if upper_h == 0 {
453            x1
454        } else {
455            x0 + (x1 - x0) * dy / upper_h
456        };
457        let (xmin, xmax) = if xa < xb { (xa, xb) } else { (xb, xa) };
458        for x in xmin..=xmax {
459            gfx.pixel(x, y, on);
460        }
461    }
462
463    // Moitié inférieure : y1 → y2
464    for y in y1..=y2 {
465        let dy = y - y0;
466        let xa = x0 + (x2 - x0) * dy / total_h;
467        let xb = if lower_h == 0 {
468            x1
469        } else {
470            x1 + (x2 - x1) * (y - y1) / lower_h
471        };
472        let (xmin, xmax) = if xa < xb { (xa, xb) } else { (xb, xa) };
473        for x in xmin..=xmax {
474            gfx.pixel(x, y, on);
475        }
476    }
477}