Skip to main content

rlvgl_widgets/motion/
background.rs

1// SPDX-License-Identifier: MIT
2//! Background patterns painted into motion jumbo buffers.
3//!
4//! A [`BackgroundPattern`] is a one-shot painter: it fills the head
5//! half of a [`crate::motion::JumboBuffer`] with a static image at
6//! crawl activation time and is then never touched again. The
7//! scrolling is implemented by the crawl engine reading sliding
8//! windows out of the pre-painted jumbo buffer.
9//!
10//! Keeping patterns as a trait means additional variants (solid
11//! colour, gradient, bitmap wallpaper, parallax layers, …) can be
12//! dropped in without touching the crawl engine. [`StarField`] is the
13//! first implementation and reproduces the classic
14//! [`star_crawl`](crate::motion::crawl::star_crawl) look: a dark
15//! blue-black background scattered with 200 stars.
16
17use rlvgl_platform::blit::{PixelFmt, Surface};
18
19/// One-shot painter for a motion background.
20///
21/// Implementations populate a [`Surface`] with a static pattern. The
22/// surface is a view over the head half of a
23/// [`crate::motion::JumboBuffer`]; the crawl engine copies the head
24/// into the tail after the pattern has been painted so scrolling can
25/// wrap seamlessly.
26pub trait BackgroundPattern {
27    /// Paint this pattern into `surface`.
28    ///
29    /// The function is called **once** at crawl activation time, not
30    /// every frame. Implementations may therefore be slow (CPU plot,
31    /// loops, etc.) without impacting runtime frame rate.
32    fn paint(&self, surface: &mut Surface<'_>);
33}
34
35/// Procedural starfield background (classic Star Wars crawl look).
36///
37/// Scatters `star_count` points across a dark background using a
38/// simple xorshift PRNG so the output is deterministic for a given
39/// `prng_seed`. The background colour is an ARGB8888 constant; stars
40/// are a random greyscale brightness drawn from `brightness_range`.
41///
42/// Only the ARGB8888 pixel format is currently supported. Other
43/// formats are a no-op.
44#[derive(Copy, Clone, Debug)]
45pub struct StarField {
46    /// Number of stars to scatter. Hardware default is 200.
47    pub star_count: u16,
48    /// xorshift32 PRNG seed. Fixed seed → deterministic output.
49    pub prng_seed: u32,
50    /// Background colour packed as 0xAARRGGBB.
51    pub bg_color: u32,
52    /// `(min, max)` greyscale value range for star brightness.
53    pub brightness: (u8, u8),
54}
55
56impl Default for StarField {
57    fn default() -> Self {
58        Self {
59            star_count: 200,
60            prng_seed: 0xDEAD_BEEF,
61            // 0xFF0A0A20 — matches the STM32H747I-DISCO star crawl.
62            bg_color: 0xFF0A_0A20,
63            brightness: (128, 255),
64        }
65    }
66}
67
68impl StarField {
69    /// Create a custom starfield configuration.
70    #[inline]
71    pub const fn new(star_count: u16, prng_seed: u32, bg_color: u32, brightness: (u8, u8)) -> Self {
72        Self {
73            star_count,
74            prng_seed,
75            bg_color,
76            brightness,
77        }
78    }
79}
80
81impl BackgroundPattern for StarField {
82    fn paint(&self, surface: &mut Surface<'_>) {
83        if surface.format != PixelFmt::Argb8888 {
84            return;
85        }
86        let bpp = 4usize;
87        let stride = surface.stride;
88        let w = surface.width as usize;
89        let h = surface.height as usize;
90        if w == 0 || h == 0 {
91            return;
92        }
93
94        // Background fill. Write bytes in little-endian BGRA order so
95        // the final u32 read matches the packed ARGB8888 constant.
96        let bg = self.bg_color.to_le_bytes();
97        for y in 0..h {
98            let row_start = y * stride;
99            for x in 0..w {
100                let off = row_start + x * bpp;
101                if off + bpp > surface.buf.len() {
102                    return;
103                }
104                surface.buf[off..off + bpp].copy_from_slice(&bg);
105            }
106        }
107
108        // Scatter stars. xorshift32 is cheap and deterministic; we use
109        // it both for position and brightness.
110        let mut rng = self.prng_seed.max(1); // xorshift breaks on 0
111        let (b_lo, b_hi) = self.brightness;
112        let b_range = b_hi.saturating_sub(b_lo) as u32 + 1;
113        for _ in 0..self.star_count {
114            rng ^= rng << 13;
115            rng ^= rng >> 17;
116            rng ^= rng << 5;
117            let xy = rng as usize % (w * h);
118            let x = xy % w;
119            let y = xy / w;
120            rng ^= rng << 13;
121            rng ^= rng >> 17;
122            rng ^= rng << 5;
123            let brightness = b_lo.saturating_add((rng % b_range) as u8);
124            let off = y * stride + x * bpp;
125            if off + bpp > surface.buf.len() {
126                continue;
127            }
128            // BGRA little-endian: [B, G, R, A]
129            surface.buf[off] = brightness;
130            surface.buf[off + 1] = brightness;
131            surface.buf[off + 2] = brightness;
132            surface.buf[off + 3] = 0xFF;
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use alloc::vec;
141
142    fn make_surface(w: u32, h: u32, buf: &mut [u8]) -> Surface<'_> {
143        Surface::new(buf, w as usize * 4, PixelFmt::Argb8888, w, h)
144    }
145
146    #[test]
147    fn paints_background_colour() {
148        let mut buf = vec![0u8; 8 * 4 * 4];
149        let sf = StarField {
150            star_count: 0,
151            ..Default::default()
152        };
153        sf.paint(&mut make_surface(8, 4, &mut buf));
154        // Every pixel should equal the default bg_color = 0xFF0A0A20.
155        for px in buf.chunks_exact(4) {
156            assert_eq!(px, &0xFF0A_0A20u32.to_le_bytes());
157        }
158    }
159
160    #[test]
161    fn scatters_stars_deterministically() {
162        let mut buf_a = vec![0u8; 64 * 64 * 4];
163        let mut buf_b = vec![0u8; 64 * 64 * 4];
164        let sf = StarField::default();
165        sf.paint(&mut make_surface(64, 64, &mut buf_a));
166        sf.paint(&mut make_surface(64, 64, &mut buf_b));
167        assert_eq!(buf_a, buf_b, "same seed must produce identical output");
168    }
169
170    #[test]
171    fn different_seed_yields_different_output() {
172        let mut buf_a = vec![0u8; 64 * 64 * 4];
173        let mut buf_b = vec![0u8; 64 * 64 * 4];
174        let sf_a = StarField::default();
175        let sf_b = StarField {
176            prng_seed: 0xCAFE_F00D,
177            ..Default::default()
178        };
179        sf_a.paint(&mut make_surface(64, 64, &mut buf_a));
180        sf_b.paint(&mut make_surface(64, 64, &mut buf_b));
181        assert_ne!(buf_a, buf_b, "different seed should scatter differently");
182    }
183
184    #[test]
185    fn at_least_one_star_has_non_bg_pixel() {
186        let mut buf = vec![0u8; 64 * 64 * 4];
187        let sf = StarField::default();
188        sf.paint(&mut make_surface(64, 64, &mut buf));
189        // At least one pixel should differ from the bg colour.
190        let bg = 0xFF0A_0A20u32.to_le_bytes();
191        let has_star = buf.chunks_exact(4).any(|px| px != bg);
192        assert!(has_star, "starfield produced no visible stars");
193    }
194
195    #[test]
196    fn non_argb_format_is_no_op() {
197        let mut buf = vec![0xAAu8; 16 * 16 * 2];
198        {
199            let mut surface = Surface::new(&mut buf, 16 * 2, PixelFmt::Rgb565, 16, 16);
200            StarField::default().paint(&mut surface);
201        }
202        // Unchanged.
203        assert!(buf.iter().all(|&b| b == 0xAA));
204    }
205
206    #[test]
207    fn zero_seed_does_not_panic() {
208        let mut buf = vec![0u8; 16 * 16 * 4];
209        let sf = StarField {
210            prng_seed: 0,
211            ..Default::default()
212        };
213        sf.paint(&mut make_surface(16, 16, &mut buf));
214    }
215}