diff --git a/eg-next/Cargo.toml b/eg-next/Cargo.toml index 4178145..6cfb84d 100644 --- a/eg-next/Cargo.toml +++ b/eg-next/Cargo.toml @@ -11,6 +11,7 @@ embedded-graphics = { git = "https://github.com/embedded-graphics/embedded-graph%20embedded-graphics-simulator%20=%20%7B%20git%20="https://github.com/embedded-graphics/simulator.git" } tinybmp = { git = "https://github.com/embedded-graphics/tinybmp.git" } tinytga = { git = "https://github.com/embedded-graphics/tinytga.git" } +rand = "0.8.3" [patch.crates-io] embedded-graphics = { git = "https://github.com/embedded-graphics/embedded-graphics.git" } diff --git a/eg-next/examples/anim8.rs b/eg-next/examples/anim8.rs new file mode 100644 index 0000000..dea17cc --- /dev/null +++ b/eg-next/examples/anim8.rs @@ -0,0 +1,394 @@ +//! # Example: Analog Clock +//! +//! ![Screenshot of clock example]( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfwAAAH9AQMAAADFwFz1AAAABlBMVEUAAAD///+l2Z/dAAAFM0lEQVR42u3dYWrjMBAF4Afz1zBX8QEEc/WBvdAcQKBNussK3EbVpDNSt+SRYMWWPrBsybQJCK+88sp/EWm32BagN+/EcoBuLRUE8K2wA6B7c9yBN2I9QK3+2eItrS0HWv0L9c9LgV6f+p6lADdcAWq6AuiVO9DRhYBYty571wBc8RGApquApkM4H+A6lvOBpkM6H+A6tvOBpkM8H6A61BcAosMetnSA6olBzpYOsI0B0Wyg4cAgSjUZIPsEgCQDoqCRD7DlAvX2Hlyo414lFSD7HJAsoI/lc3An9xGdBFRMpCYCZDOAJAKsMwBrHiCYCVka0I+MI0GAvw97vSxAMBeyLKBiMjUJIJsFJAjwX8VeMwcQzIYsB6iYTk0ByOYBCQL8V7HXzQAE8yHLAAocSQHMA5QEgNQDcALAPkDjAYEnZPFAhSs1HjAfUMIBUh/A4QB7AY0GBL6QRQMFzoQD5gVKMEBuQKIB9QIcDDC8IY0FxA9YLFDgTjBQ/UCNBcwPlFCA1A9wKMBPAE1DgeI/AwoFxN+LJ1kkUHD655NQoLrntAOokYAB6h9NgQAZnohEAvoMwFHA+9HcHuUCaCCAZwAKBOQ5wOKAcgGU7q/WLoULgB8FXMZSlVqkisq1cAHKDwJIrzdSlXZvdysU6YUrwD8ZqHR/3VKpKeRvgVoawHgOIE0DfrX7S6X35q/bpkoaIM8CFgWUy4G3VnegSr21fSt8BOAnA2+75O9cet9WQG7lNMCeBUoW0B7kBwOkzwIcB/iSD+h2oKwGnv8XMWkSgHM7wNsB2Fqg4OlzsDQAuh0o2wHS3QDO7QCvBGxw7JOUTODYDpDuBlC2A6S7AZzbAd4OwBYB/XL5xxMnA6S7AZTtAG0HYNsB3g7AWDcDR6ubAfoGQFsAtGbPAdLal4Fv0gcjAK1+EfgGN9IXgcK6GVBsBhi7gXM7oDdjK8DYDZxrANioD8dASQYYO4F/bY+NAOlu4MBu4NwNkG4FekvKBUZ/WtkEYJkAYTdwbAesz6x7AMJu4FgJ8INDnwOkeQBhN1B68dwD6FKAdNCHQ4CRBpTtgO4G+DKws4H3T9dzEihpgO4GGKuBMujDIWBZgF6G9nKAsRyQQR+OAAoD+DJXTwOaAxxYCPTCaBLVwYyWAZDuBg5sAGDosXmgpACELUAv4XB8fWwpgO0B5NKHcwAFAtz70ANoIPCvaA6AA4E+O3q+QWfEAf2CeAAJBPAUUCKBCqgPOIAaCRQwfAApLBIQnHD+iOCkUIBJvQCThgIVXgAtFCD1A4xIYDyaPtZLLFD9QB0CbqD4AYsFxA1QMMB+QGOB/nH26coIBswLSDAAN1CigeIFLBoQJ0DhAKvv6coaDZAXQDQA8wElHqg+oMYD4gLI4gF2/daTNR4gH4B4AOYBSgZQPIBlAOIAKALw3En6vm4GQDYPCDIA1Hmg5gAyDZDlAKyzAGsOQDb7fBfkAKizQM0CZBIgywJY5wDWLIBsDhBkAZCp5ztZHsD6ALZxLT/g7UU9LicaALiGE4EulfIAUQD6wVXUfj3ZMgGy2xuPAep9mASgfg7UXEAUxwCAgi0XIMM5Ak4IcgE0nKNb+aSaDbDpaDgfotkAVR1NaUdDBOBfWkj7GeYDVEdAQz6Apo+frlxTgUGls+OpwONaZ7dXAFzfA51eAfR6HehyCOBfdu/o8BIAYleg710DUNP3ADesAnrlDnR0EYBWL0Dfkw90oQPU24cA/uVYqTV8Ffj/1rT9Buv6foO1jb/D+s6vvPKKI78B89G0YRkxkl8AAAAASUVORK5CYII=) +//! +//! This example shows some more advanced usage of Embedded Graphics. It draws a round clock face +//! with hour, minute and second hands. A digital clock is drawn in the middle of the clock. The +//! whole thing is updated with your computer's local time every 50ms. + +use chrono::{Local, Timelike}; +use core::f32::consts::PI; +use embedded_graphics::{ + image::Image, + mono_font::{ascii::FONT_9X15, MonoTextStyle}, + pixelcolor::{Rgb565, Rgb888}, + prelude::*, + primitives::{ + self, Arc, Circle, Line, Polyline, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, + Sector, Triangle, + }, + text::Text, +}; +use embedded_graphics_simulator::{ + OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window, +}; +use rand::{thread_rng, Rng}; +use std::{ + thread, + time::{Duration, Instant}, +}; +use tinybmp::Bmp; + +fn polar(circle: &Circle, angle: f32, radius: i32) -> Point { + let radius = radius as f32; + + circle.center() + + Point::new( + (angle.sin() * radius) as i32, + -(angle.cos() * radius) as i32, + ) +} + +fn draw_static_crosshair(target: &mut D) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let center = Point::new(80, 80); + + // Outside + Circle::with_center(center, 149) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + // Inside + Circle::with_center(center, 15) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + // Center crosshair point + Pixel(center, Rgb888::CSS_GREEN).draw(target)?; + + // Crosshair lines + { + let offs = Size::new_equal(20); + let len = Size::new_equal(20); + + let line_style = PrimitiveStyle::with_stroke(Rgb888::CSS_LIME_GREEN, 1); + + Line::new( + center - offs.y_axis(), + center - offs.y_axis() - len.y_axis(), + ) + .into_styled(line_style) + .draw(target)?; + + Line::new( + center - offs.x_axis(), + center - offs.x_axis() - len.x_axis(), + ) + .into_styled(line_style) + .draw(target)?; + + Line::new( + center + offs.x_axis(), + center + offs.x_axis() + len.x_axis(), + ) + .into_styled(line_style) + .draw(target)?; + + Line::new( + center + offs.y_axis(), + center + offs.y_axis() + len.y_axis(), + ) + .into_styled(line_style) + .draw(target)?; + } + + Ok(()) +} + +fn draw_dynamic_crosshair(target: &mut D, t: Duration) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let center = Point::new(80, 80); + + let angle = (t.as_millis() / 10 % 360) as u32; + let angle = (angle as f32).deg(); + + Arc::with_center(center, 149, 0.0.deg() + angle, 45.0.deg()) + .into_styled( + PrimitiveStyleBuilder::new() + .stroke_alignment(primitives::StrokeAlignment::Outside) + .stroke_width(5) + .stroke_color(Rgb888::CSS_LIME_GREEN) + .build(), + ) + .draw(target)?; + + Arc::with_center(center, 149, -90.0.deg() - angle, -110.0.deg()) + .into_styled( + PrimitiveStyleBuilder::new() + .stroke_alignment(primitives::StrokeAlignment::Inside) + .stroke_width(5) + .stroke_color(Rgb888::CSS_LIME_GREEN) + .build(), + ) + .draw(target)?; + + Ok(()) +} + +fn draw_radar(target: &mut D, t: Duration) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let size = 89; + let center = Point::new(200, 200); + + let base_angle = (t.as_millis() / 5 % 360) as u32; + let angle = (base_angle as f32).deg(); + + let radar = Circle::with_center(center, size); + + radar + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + Sector::with_center(center, size, 90.0.deg() + angle, -60.0.deg()) + .into_styled( + PrimitiveStyleBuilder::new() + .fill_color(Rgb888::CSS_LIME_GREEN) + .build(), + ) + .draw(target)?; + + { + let pos = polar(&radar, 0.0, 20); + let brightness = 255u32.saturating_sub(base_angle) as u8; + let color = Rgb888::new(0, brightness, 0); + + Circle::with_center(pos, 3) + .into_styled(PrimitiveStyle::with_fill(color)) + .draw(target)?; + } + + Ok(()) +} + +fn draw_scanners(target: &mut D, t: Duration) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let frame = Rectangle::new(Point::new(200 - 89 / 2 + 10, 10), Size::new(79, 10)); + + frame + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + let w = frame.bounding_box().size.width - 10; + + let pos = ((t.as_millis() as u32 / 20) % w * 2) as u32; + + let pos = if pos > w { + // Reverse + w + (w - pos) + } else { + // Forward + pos + }; + + Rectangle::new( + Point::new(200 - 89 / 2 + 10 + pos as i32, 10), + Size::new(10, 10), + ) + .into_styled(PrimitiveStyle::with_fill(Rgb888::CSS_LIME_GREEN)) + .draw(target)?; + + // --- + + let frame = Rectangle::new(Point::new(200 - 89 / 2 + 10, 30), Size::new(79, 10)); + + frame + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + let w = frame.bounding_box().size.width - 10; + + let pos = ((t.as_millis() as u32 / 20 + 20) % w * 2) as u32; + + let pos = if pos > w { + // Reverse + w + (w - pos) + } else { + // Forward + pos + }; + + Rectangle::new( + Point::new( + 200 - 89 / 2 + 10 + pos as i32, + frame.bounding_box().top_left.y, + ), + Size::new(10, 10), + ) + .into_styled(PrimitiveStyle::with_fill(Rgb888::CSS_LIME_GREEN)) + .draw(target)?; + + Ok(()) +} + +fn draw_horizontal_rule(target: &mut D, t: Duration, lerp: &mut Lerp) -> Result<(), D::Error> +where + D: DrawTarget, +{ + Polyline::new(&[ + Point::new(10, 200 - 5), + Point::new(10, 200), + Point::new(130, 200), + Point::new(130, 200 - 5), + ]) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_GREEN, 1)) + .draw(target)?; + + Triangle::new(Point::zero(), Point::new(5, 10), Point::new(-5, 10)) + .translate(Point::new( + 10 + lerp.position(t.as_millis() as u64) as i32, + 200, + )) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_LIME_GREEN, 1)) + .draw(target)?; + + Ok(()) +} + +struct Invuln { + image: Bmp<'static, Rgb565>, +} + +impl Invuln { + fn new(image: Bmp<'static, Rgb565>) -> Self { + Self { image } + } + + fn draw(&mut self, display: &mut D, t: Duration) -> Result<(), D::Error> + where + D: DrawTarget, + { + let elapsed = t.as_millis() as u32; + + let index = (elapsed / 250) % 4; + + let mut display = display.color_converted(); + + let frame = self.image.sub_image(&Rectangle::new( + Point::new(self.image.size().height as i32 * index as i32, 0), + Size::new_equal(self.image.size().height), + )); + + Image::new(&frame, Point::new(200, 100)).draw(&mut display)?; + + Ok(()) + } +} + +struct Floater { + item: Invuln, +} + +impl Floater { + fn new(item: Invuln) -> Self { + Self { item } + } + + fn draw(&mut self, display: &mut D, t: Duration) -> Result<(), D::Error> + where + D: DrawTarget, + { + let elapsed = t.as_millis() as u32; + + let pos = ((elapsed as f32 / 200.0).sin() - 1.0) / 2.0 * 15.0; + let pos = pos as i32; + + let mut display = display.translated(Point::new(0, pos)); + + self.item.draw(&mut display, t)?; + + Ok(()) + } +} + +struct Lerp { + is_animating: bool, + duration: u32, + final_position: u32, + start_position: u32, + start: u64, +} + +impl Lerp { + fn position(&mut self, t: u64) -> u32 { + let t = t - self.start; + let t = (t as u32) as f32 / self.duration as f32; + + if t >= 1.0 { + self.is_animating = false; + } + + // Floating point LERP + let pos = (1.0 - t) * self.start_position as f32 + t * self.final_position as f32; + + pos as u32 + } + + fn move_target(&mut self, new_start: u64, new_target: u32) { + self.start_position = self.final_position; + self.final_position = new_target; + self.start = new_start; + + println!( + "Reset to {} -> {}", + self.start_position, self.final_position + ); + + self.is_animating = true; + } +} + +fn main() -> Result<(), core::convert::Infallible> { + let mut display = SimulatorDisplay::::new(Size::new(256, 256)); + + let output_settings = OutputSettingsBuilder::new().scale(2).build(); + let mut window = Window::new("Greebles", &output_settings); + + let start = Instant::now(); + + let mut h_state = Lerp { + is_animating: true, + duration: 500, + start_position: 0, + final_position: 50, + start: 0, + }; + + let mut rng = thread_rng(); + + let invuln: Bmp = Bmp::from_slice(include_bytes!("./assets/invuln.bmp")).unwrap(); + let mut invuln = Floater::new(Invuln::new(invuln)); + + 'running: loop { + display.clear(Rgb888::BLACK)?; + + draw_static_crosshair(&mut display)?; + draw_dynamic_crosshair(&mut display, start.elapsed())?; + + draw_radar(&mut display, start.elapsed())?; + + draw_scanners(&mut display, start.elapsed())?; + + draw_horizontal_rule(&mut display, start.elapsed(), &mut h_state)?; + + invuln.draw(&mut display, start.elapsed())?; + + if !h_state.is_animating { + h_state.move_target(start.elapsed().as_millis() as u64, rng.gen_range(0..=120)); + } + + window.update(&display); + + if window.events().any(|e| e == SimulatorEvent::Quit) { + break 'running Ok(()); + } + thread::sleep(Duration::from_millis(16)); + } +} diff --git a/eg-next/examples/assets/chopper.bmp b/eg-next/examples/assets/chopper.bmp new file mode 100644 index 0000000..c583950 Binary files /dev/null and b/eg-next/examples/assets/chopper.bmp differ diff --git a/eg-next/examples/assets/chopper.xcf b/eg-next/examples/assets/chopper.xcf new file mode 100644 index 0000000..48e6186 Binary files /dev/null and b/eg-next/examples/assets/chopper.xcf differ diff --git a/eg-next/examples/assets/invuln.bmp b/eg-next/examples/assets/invuln.bmp new file mode 100644 index 0000000..b3a5203 Binary files /dev/null and b/eg-next/examples/assets/invuln.bmp differ diff --git a/eg-next/examples/assets/soulsphere.bmp b/eg-next/examples/assets/soulsphere.bmp new file mode 100644 index 0000000..afb2cf5 Binary files /dev/null and b/eg-next/examples/assets/soulsphere.bmp differ diff --git a/eg-next/examples/choppa.rs b/eg-next/examples/choppa.rs new file mode 100644 index 0000000..bfad980 --- /dev/null +++ b/eg-next/examples/choppa.rs @@ -0,0 +1,363 @@ +//! # Example: Analog Clock +//! +//! ![Screenshot of clock example]( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfwAAAH9AQMAAADFwFz1AAAABlBMVEUAAAD///+l2Z/dAAAFM0lEQVR42u3dYWrjMBAF4Afz1zBX8QEEc/WBvdAcQKBNussK3EbVpDNSt+SRYMWWPrBsybQJCK+88sp/EWm32BagN+/EcoBuLRUE8K2wA6B7c9yBN2I9QK3+2eItrS0HWv0L9c9LgV6f+p6lADdcAWq6AuiVO9DRhYBYty571wBc8RGApquApkM4H+A6lvOBpkM6H+A6tvOBpkM8H6A61BcAosMetnSA6olBzpYOsI0B0Wyg4cAgSjUZIPsEgCQDoqCRD7DlAvX2Hlyo414lFSD7HJAsoI/lc3An9xGdBFRMpCYCZDOAJAKsMwBrHiCYCVka0I+MI0GAvw97vSxAMBeyLKBiMjUJIJsFJAjwX8VeMwcQzIYsB6iYTk0ByOYBCQL8V7HXzQAE8yHLAAocSQHMA5QEgNQDcALAPkDjAYEnZPFAhSs1HjAfUMIBUh/A4QB7AY0GBL6QRQMFzoQD5gVKMEBuQKIB9QIcDDC8IY0FxA9YLFDgTjBQ/UCNBcwPlFCA1A9wKMBPAE1DgeI/AwoFxN+LJ1kkUHD655NQoLrntAOokYAB6h9NgQAZnohEAvoMwFHA+9HcHuUCaCCAZwAKBOQ5wOKAcgGU7q/WLoULgB8FXMZSlVqkisq1cAHKDwJIrzdSlXZvdysU6YUrwD8ZqHR/3VKpKeRvgVoawHgOIE0DfrX7S6X35q/bpkoaIM8CFgWUy4G3VnegSr21fSt8BOAnA2+75O9cet9WQG7lNMCeBUoW0B7kBwOkzwIcB/iSD+h2oKwGnv8XMWkSgHM7wNsB2Fqg4OlzsDQAuh0o2wHS3QDO7QCvBGxw7JOUTODYDpDuBlC2A6S7AZzbAd4OwBYB/XL5xxMnA6S7AZTtAG0HYNsB3g7AWDcDR6ubAfoGQFsAtGbPAdLal4Fv0gcjAK1+EfgGN9IXgcK6GVBsBhi7gXM7oDdjK8DYDZxrANioD8dASQYYO4F/bY+NAOlu4MBu4NwNkG4FekvKBUZ/WtkEYJkAYTdwbAesz6x7AMJu4FgJ8INDnwOkeQBhN1B68dwD6FKAdNCHQ4CRBpTtgO4G+DKws4H3T9dzEihpgO4GGKuBMujDIWBZgF6G9nKAsRyQQR+OAAoD+DJXTwOaAxxYCPTCaBLVwYyWAZDuBg5sAGDosXmgpACELUAv4XB8fWwpgO0B5NKHcwAFAtz70ANoIPCvaA6AA4E+O3q+QWfEAf2CeAAJBPAUUCKBCqgPOIAaCRQwfAApLBIQnHD+iOCkUIBJvQCThgIVXgAtFCD1A4xIYDyaPtZLLFD9QB0CbqD4AYsFxA1QMMB+QGOB/nH26coIBswLSDAAN1CigeIFLBoQJ0DhAKvv6coaDZAXQDQA8wElHqg+oMYD4gLI4gF2/daTNR4gH4B4AOYBSgZQPIBlAOIAKALw3En6vm4GQDYPCDIA1Hmg5gAyDZDlAKyzAGsOQDb7fBfkAKizQM0CZBIgywJY5wDWLIBsDhBkAZCp5ztZHsD6ALZxLT/g7UU9LicaALiGE4EulfIAUQD6wVXUfj3ZMgGy2xuPAep9mASgfg7UXEAUxwCAgi0XIMM5Ak4IcgE0nKNb+aSaDbDpaDgfotkAVR1NaUdDBOBfWkj7GeYDVEdAQz6Apo+frlxTgUGls+OpwONaZ7dXAFzfA51eAfR6HehyCOBfdu/o8BIAYleg710DUNP3ADesAnrlDnR0EYBWL0Dfkw90oQPU24cA/uVYqTV8Ffj/1rT9Buv6foO1jb/D+s6vvPKKI78B89G0YRkxkl8AAAAASUVORK5CYII=) +//! +//! This example shows some more advanced usage of Embedded Graphics. It draws a round clock face +//! with hour, minute and second hands. A digital clock is drawn in the middle of the clock. The +//! whole thing is updated with your computer's local time every 50ms. + +use chrono::{Local, Timelike}; +use core::f32::consts::PI; +use embedded_graphics::{ + image::Image, + mono_font::{ + ascii::{FONT_10X20, FONT_6X13, FONT_7X13, FONT_8X13, FONT_9X15}, + MonoTextStyle, + }, + pixelcolor::{Rgb565, Rgb888}, + prelude::*, + primitives::{ + self, Arc, Circle, Line, Polyline, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, + Sector, Triangle, + }, + text::{Alignment, Baseline, Text, TextStyleBuilder}, +}; +use embedded_graphics_simulator::{ + sdl2::Keycode, OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window, +}; +use rand::{thread_rng, Rng}; +use std::{ + thread, + time::{Duration, Instant}, +}; +use tinybmp::Bmp; + +fn polar(circle: &Circle, angle: f32, radius: i32) -> Point { + let radius = radius as f32; + + circle.center() + + Point::new( + (angle.sin() * radius) as i32, + -(angle.cos() * radius) as i32, + ) +} + +enum State { + Off, + TitleOpening { start: Duration, offset: u32 }, + TitleIdle, + Running { velocity: i32, position: i32 }, +} + +struct ChopperSprite { + image: Bmp<'static, Rgb565>, +} + +impl ChopperSprite { + const SIZE: Size = Size::new(31, 10); + + fn new(image: Bmp<'static, Rgb565>) -> Self { + Self { image } + } + + fn draw(&mut self, display: &mut D, t: Duration, position: Point) -> Result<(), D::Error> + where + D: DrawTarget, + { + let elapsed = t.as_millis() as u32; + + let index = (elapsed / 30) % 9; + + let index = if index >= 5 { + // Reverse + 9 - index + } else { + // Forward + index + }; + + let mut display = display.color_converted(); + + let frame = self.image.sub_image(&Rectangle::new( + Point::new(0, Self::SIZE.height as i32 * index as i32), + Self::SIZE, + )); + + Image::new(&frame, position).draw(&mut display)?; + + Ok(()) + } +} + +const NUM_POINTS: usize = 5; + +struct Stage { + position: u32, + + /// `(offset, height)` + points: [(i32, u32); NUM_POINTS], +} + +impl Stage { + fn new() -> Self { + let mut rng = thread_rng(); + + let mut points = [(0, 0); NUM_POINTS]; + + (0..NUM_POINTS).for_each(|idx| { + let offset = rng.gen_range(-50i32..=50); + let height = rng.gen_range(100u32..=150); + + points[idx] = (offset, height) + }); + + Self { + position: 0, + points, + } + } + + fn draw(&mut self, display: &mut D, t: Duration) -> Result<(), D::Error> + where + D: DrawTarget, + { + display.clear(Rgb888::CSS_LIME)?; + + let bb = display.bounding_box(); + + let spacing = bb.size.width / (NUM_POINTS - 2) as u32; + + let center = bb.center(); + + for (idx, parts) in self.points.windows(2).enumerate() { + if let [(offs_1, height_1), (offs_2, height_2)] = parts { + let x1 = (idx as u32 * spacing) as i32; + let x2 = ((idx as u32 + 1) * spacing) as i32; + + let a = Point::new(x1, center.y + offs_1 - *height_1 as i32 / 2); + let b = Point::new(x1, center.y + offs_1 + *height_1 as i32 / 2); + let c = Point::new(x2, center.y + offs_2 - *height_2 as i32 / 2); + let d = Point::new(x2, center.y + offs_2 + *height_2 as i32 / 2); + + let style = PrimitiveStyle::with_fill(Rgb888::BLACK); + + Triangle::new(a, b, c).into_styled(style).draw(display)?; + Triangle::new(b, c, d).into_styled(style).draw(display)?; + } + } + + Ok(()) + } +} + +struct Game { + state: State, + chopper: ChopperSprite, + stage: Stage, +} + +impl Game { + fn new(chopper: ChopperSprite) -> Self { + Self { + state: State::Off, + chopper, + stage: Stage::new(), + } + } + + fn update(&mut self, target: &mut D, t: Duration) -> Result<(), D::Error> + where + D: DrawTarget, + { + let new_state = match &mut self.state { + State::Off => Some(State::TitleOpening { + start: t, + offset: 0, + }), + State::TitleOpening { start, offset } => { + if *offset > target.bounding_box().size.height / 2 { + Some(State::TitleIdle) + } else { + Self::draw_title(target, *offset, target.bounding_box().size.height / 2)?; + + *offset = ((t - *start).as_millis() / 8) as u32; + + None + } + } + State::TitleIdle => { + Self::draw_title(target, 0, 0)?; + + let opacity = 128.0 + ((((t.as_millis() / 100) as f32).sin() + 1.0) / 2.0) * 127.0; + + Self::draw_start_button(target, Rgb888::new(0, opacity as u8, 0))?; + + None + } + State::Running { velocity, position } => { + // *position -= *velocity; + + self.stage.draw(target, t)?; + + self.chopper.draw( + target, + t, + target.bounding_box().center().y_axis() + Point::new(15, *position), + )?; + + None + } + }; + + if let Some(new_state) = new_state { + self.state = new_state; + } + + Ok(()) + } + + fn key_down(&mut self, keycode: Keycode) { + match keycode { + Keycode::R => self.reset(), + + Keycode::Space => { + let new_state = match &mut self.state { + State::TitleIdle => Some(State::Running { + velocity: 0, + position: 0, + }), + State::Running { velocity, .. } => { + *velocity = 3; + + None + } + _ => None, + }; + + if let Some(new_state) = new_state { + self.state = new_state; + } + } + + _ => (), + } + } + + fn key_up(&mut self, keycode: Keycode) { + match keycode { + Keycode::Space => { + let new_state = match &mut self.state { + State::Running { velocity, .. } => { + *velocity = -5; + + None + } + _ => None, + }; + + if let Some(new_state) = new_state { + self.state = new_state; + } + } + _ => (), + } + } + + fn reset(&mut self) { + self.state = State::Off + } + + fn draw_start_button(target: &mut D, color: Rgb888) -> Result<(), D::Error> + where + D: DrawTarget, + { + let small_font = MonoTextStyle::new(&FONT_6X13, color); + + Text::with_text_style( + "Press space to start", + target.bounding_box().center() + FONT_10X20.character_size.y_axis(), + small_font, + TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Top) + .build(), + ) + .draw(target)?; + + Ok(()) + } + + fn draw_title(target: &mut D, offset: u32, limit: u32) -> Result<(), D::Error> + where + D: DrawTarget, + { + let limit = limit as i32; + + let small_font = MonoTextStyle::new(&FONT_6X13, Rgb888::CSS_LIME); + let big_font = MonoTextStyle::new(&FONT_10X20, Rgb888::CSS_LIME); + + let base = TextStyleBuilder::new().alignment(Alignment::Center); + + let offset = Point::new(0, offset as i32); + + Text::with_text_style( + "GET TO DA", + target.bounding_box().center() - Point::new(0, limit) + offset, + small_font, + base.baseline(Baseline::Bottom).build(), + ) + .draw(target)?; + + Text::with_text_style( + "CHOPPA", + target.bounding_box().center() + Point::new(0, limit) - offset, + big_font, + base.baseline(Baseline::Top).build(), + ) + .draw(target)?; + + Ok(()) + } +} + +fn main() -> Result<(), core::convert::Infallible> { + let mut display = SimulatorDisplay::::new(Size::new(384, 256)); + + let output_settings = OutputSettingsBuilder::new().scale(2).build(); + let mut window = Window::new("CHOPPA", &output_settings); + + let start = Instant::now(); + + let chopper: Bmp = Bmp::from_slice(include_bytes!("./assets/chopper.bmp")).unwrap(); + let chopper = ChopperSprite::new(chopper); + + let mut game = Game::new(chopper); + + 'running: loop { + display.clear(Rgb888::BLACK)?; + + game.update(&mut display, start.elapsed())?; + + window.update(&display); + + // if window.events().any(|e| e == SimulatorEvent::Quit) { + // break 'running Ok(()); + // } + + for event in window.events() { + match event { + SimulatorEvent::Quit => break 'running Ok(()), + SimulatorEvent::KeyDown { keycode, .. } => { + game.key_down(keycode); + } + SimulatorEvent::KeyUp { keycode, .. } => { + game.key_up(keycode); + } + + _ => {} + } + } + + thread::sleep(Duration::from_millis(16)); + } +}