diff --git a/Cargo.lock b/Cargo.lock index c2864ef..a738340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,7 @@ dependencies = [ "embedded-sprites", "heapless 0.9.2", "profont", + "rand", "tinybmp", ] diff --git a/Cargo.toml b/Cargo.toml index 59e9e4f..7b50a33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] -#buoyant = "0.6.0-alpha.0" buoyant = "0.5.3" -#buoyant = {path = "buoyant" } embedded-graphics = "0.8.1" embedded-graphics-simulator = "0.8.0" embedded-sprites = "0.2.0" heapless = "0.9.2" profont = "0.7.0" +rand = "0.9.2" tinybmp = "0.6.0" diff --git a/assets/co2-icon.bmp b/assets/co2-icon.bmp index a74e213..e69df2d 100644 Binary files a/assets/co2-icon.bmp and b/assets/co2-icon.bmp differ diff --git a/assets/co2-icon.svg b/assets/co2-icon.svg index 391b8ec..ed04641 100644 --- a/assets/co2-icon.svg +++ b/assets/co2-icon.svg @@ -24,7 +24,7 @@ inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="7.6922777" - inkscape:cx="29.510115" + inkscape:cx="29.575115" inkscape:cy="28.27511" inkscape:window-width="1916" inkscape:window-height="1032" @@ -40,7 +40,7 @@ id="layer1" transform="translate(0.08272917,0.15224179)"> f32 { + ((x - x_min) / (x_max - x_min)) * (y_max - y_min) + y_min +} + +const DEFAULT_LUT: [Rgb565; 5] = [ + Rgb565::new(0, 16, 11), + Rgb565::new(11, 20, 17), + Rgb565::new(23, 20, 18), + Rgb565::new(31, 24, 12), + Rgb565::new(31, 41, 0), +]; + +fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: f32) -> Rgb565 { + Rgb565::new( + (a.r() as f32 * (1. - x)) as u8 + (b.r() as f32 * x) as u8, + (a.g() as f32 * (1. - x)) as u8 + (b.g() as f32 * x) as u8, + (a.b() as f32 * (1. - x)) as u8 + (b.b() as f32 * x) as u8, + ) +} + +fn color_lut(mut x: f32, colors: &[Rgb565]) -> Rgb565 { + if x == 1. { + return colors[colors.len() - 1]; + } + + if x == 0. { + return colors[0]; + } + + x *= (colors.len() - 1) as f32; + + let index = x.floor(); + let interp = x - index; + + rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp) +} + +pub fn graph_data + GetPixel>( + data: &[f32], + target: &mut T, +) { + let min = data.iter().copied().reduce(f32::min).unwrap_or(0.); + let max = data.iter().copied().reduce(f32::max).unwrap_or(0.); + let size = Size::new( + target.bounding_box().size.width, + target.bounding_box().size.height, + ); + + // Draw data as WHITE line + let mut start = Point::new( + 0, + map_float(data[0], min, max, size.height as f32, 0.) as i32, + ); + for (i, x) in data.iter().skip(1).enumerate() { + let point = Point::new( + map_float(i as f32, 0., data.len() as f32 - 1., 0., size.width as f32) as i32, + map_float(*x, min, max, size.height as f32, 0.) as i32, + ); + let _ = Line::new(start, point) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 2)) + .draw(target); + start = point; + } + + for x in 0..size.width { + // Start coloring from up to bottom + let mut met_curve = false; + + for y in 0..size.height { + let position = Point::new(x as i32, y as i32); + let pixel = target.pixel(position).unwrap(); + let height_factor = map_float(y as f32, 0., size.height as f32, 1., 0.); + let height_color = color_lut(height_factor, &DEFAULT_LUT); + + if pixel == Rgb565::WHITE { + let _ = Pixel(position, height_color).draw(target); + met_curve = true; + } else if met_curve && (x as i32 - y as i32) % 7 == 0 { + let _ = Pixel( + position, + rgb565_interpolate(height_color, Rgb565::BLACK, 1. - height_factor), + ) + .draw(target); + } + } + } +} + +pub fn min_indicator + GetPixel>( + data: &[f32], + target: &mut T, +) { + let size = target.bounding_box().size; + let (min_index, min) = data + .iter() + .copied() + .enumerate() + .reduce(|a, b| if a.1 < b.1 { a } else { b }) + .unwrap_or((0, 0.)); + + let min_x = map_float( + min_index as f32, + 0., + data.len() as f32, + 0., + size.width as f32, + ) as i32; + + // let _ = Line::new(Point::new(min_x, 0), Point::new(min_x, size.height as i32)) + // .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1)) + // .draw(target); + for y in 0..size.height { + if (y / 2) % 2 == 0 { + let position = Point::new(min_x, y as i32); + let _ = Pixel( + position, + rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6), + ) + .draw(target); + + // let position = Point::new(min_x + 1, y as i32); + // let _ = Pixel( + // position, + // rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6), + // ) + // .draw(target); + } + } + + let minimum_text = "Minimum"; + let font = &PROFONT_10_POINT; + + let text_start = if min_x < (size.width / 2) as i32 { + 5 + } else { + -((minimum_text.len() + 1) as i32 * font.character_size.width as i32 + 3) + }; + + let style = MonoTextStyle::new( + &PROFONT_10_POINT, + rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8), + ); + let _ = Text::new( + minimum_text, + Point::new(min_x + text_start as i32, 10), + style, + ) + .draw(target); + + let value = format!(16; "{:.1}", min).unwrap(); + let _ = Text::new( + value.as_str(), + Point::new( + min_x + text_start as i32, + 10 + font.character_size.height as i32, + ), + style, + ) + .draw(target); +} + +pub fn max_indicator + GetPixel>( + data: &[f32], + target: &mut T, +) { + let size = target.bounding_box().size; + let (max_index, max) = data + .iter() + .copied() + .enumerate() + .reduce(|a, b| if a.1 > b.1 { a } else { b }) + .unwrap_or((0, 0.)); + + let max_x = map_float( + max_index as f32, + 0., + data.len() as f32, + 0., + size.width as f32, + ) as i32; + + for y in 0..size.height { + if (y / 2) % 2 == 0 { + let position = Point::new(max_x, y as i32); + let _ = Pixel( + position, + rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6), + ) + .draw(target); + + // let position = Point::new(max_x + 1, y as i32); + // let _ = Pixel( + // position, + // rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6), + // ) + // .draw(target); + } + } + + let maximum_text = "Maximum"; + let font = &PROFONT_10_POINT; + + let text_start = if max_x < (size.width / 2) as i32 { + 5 + } else { + -((maximum_text.len() + 1) as i32 * font.character_size.width as i32 + 3) + }; + + let style = MonoTextStyle::new( + &PROFONT_10_POINT, + rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8), + ); + let _ = Text::new( + maximum_text, + Point::new( + max_x + text_start as i32, + size.height as i32 - font.character_size.height as i32 * 2, + ), + style, + ) + .draw(target); + + let value = format!(16; "{:.1}", max).unwrap(); + let _ = Text::new( + value.as_str(), + Point::new( + max_x + text_start as i32, + size.height as i32 - font.character_size.height as i32, + ), + style, + ) + .draw(target); +} diff --git a/src/main.rs b/src/main.rs index 6833498..7a5fb47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ //#![feature(unsafe_cell_access)] mod colors; +mod graph; mod images; mod views; @@ -8,7 +9,11 @@ use std::cell::UnsafeCell; use std::mem::MaybeUninit; use buoyant::view::prelude::*; +use embedded_graphics::framebuffer::Framebuffer; +use embedded_graphics::framebuffer::buffer_size; +use embedded_graphics::image::ImageRaw; use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::pixelcolor::raw::LittleEndian; use embedded_graphics::prelude::*; use embedded_graphics_simulator::OutputSettings; use embedded_graphics_simulator::SimulatorDisplay; @@ -16,6 +21,10 @@ use embedded_graphics_simulator::Window; use heapless::format; use tinybmp::Bmp; +use crate::graph::graph_data; +use crate::graph::max_indicator; +use crate::graph::min_indicator; + const BACKGROUND_COLOR: Rgb565 = Rgb565::BLACK; const DEFAULT_COLOR: Rgb565 = Rgb565::WHITE; @@ -23,22 +32,50 @@ fn main() { images::prepare_images(); let mut window = Window::new("Hello World", &OutputSettings::default()); - let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(320, 240)); + let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(240, 240)); display.clear(BACKGROUND_COLOR); - // views::menu::menu_view() - // .as_drawable(display.size(), DEFAULT_COLOR) - // .draw(&mut display) - // .unwrap(); - - views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady) + views::menu::menu_view() .as_drawable(display.size(), DEFAULT_COLOR) .draw(&mut display) .unwrap(); + // views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady) + // .as_drawable(display.size(), DEFAULT_COLOR) + // .draw(&mut display) + // .unwrap(); + + let mut dummy_data = [0.; 100]; + for i in 0..dummy_data.len() { + dummy_data[i] = rand::random::(); + } + + let smoothed = dummy_data + .windows(30) + .map(|x| x.iter().sum::() / 10.) + .collect::>(); + + let mut fb = Framebuffer::< + Rgb565, + _, + LittleEndian, + 240, + { 240 - 80 }, + { buffer_size::(240, 240) }, + >::new(); + + // graph_data(&smoothed, &mut fb); + // min_indicator(&smoothed, &mut fb); + //max_indicator(&smoothed, &mut fb); + + let img_raw = ImageRaw::::new(fb.data(), 240); + let image = embedded_graphics::image::Image::new(&img_raw, Point::new(0, 60)); + //image.draw(&mut display).unwrap(); + window.show_static(&display); } +#[derive(Clone, Copy)] pub enum MenuIndicatorType { Temperature(f32), Humidity(f32), @@ -77,6 +114,7 @@ impl MenuIndicatorType { } } +#[derive(Clone, Copy)] pub enum Tendency { Rising, Steady, diff --git a/src/views/detail.rs b/src/views/detail.rs index 0bcf0ff..69fdbaf 100644 --- a/src/views/detail.rs +++ b/src/views/detail.rs @@ -6,12 +6,18 @@ use buoyant::view::Text; use buoyant::view::VStack; use buoyant::view::View; use buoyant::view::ViewExt; +use buoyant::view::ZStack; +use buoyant::view::shape::Rectangle; +use buoyant::view::shape::ShapeExt; use embedded_graphics::pixelcolor::Rgb565; use profont::PROFONT_18_POINT; use profont::PROFONT_24_POINT; use crate::MenuIndicatorType; use crate::Tendency; +use crate::colors::BACKGROUND_COLOR; +use crate::colors::FRAME_BACKGROUD_COLOR; +use crate::colors::FRAME_STROKE; use crate::colors::FRAME_STROKE_COLOR; use crate::colors::MAIN_TEXT_COLOR; use crate::colors::SUB_TEXT_COLOR; @@ -20,33 +26,36 @@ use crate::views::icon::icon_box_view; pub fn detailed_view(indicator: MenuIndicatorType, tendency: Tendency) -> impl View { VStack::new(( - // Header - HStack::new(( - icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()), - Spacer::default().flex_frame().with_max_width(10), - tendency_indicator(tendency), - Text::new(indicator.get_value_str(), &PROFONT_24_POINT) - .foreground_color(MAIN_TEXT_COLOR), - Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT) - .foreground_color(SUB_TEXT_COLOR) + ZStack::new(( + header(indicator, tendency), + Rectangle + .corner_radius(10) + .stroked(FRAME_STROKE) + .foreground_color(BACKGROUND_COLOR) .flex_frame() - .with_infinite_max_height() - .with_vertical_alignment(VerticalAlignment::Bottom) - .with_max_height(25), - Spacer::default(), - Text::new("Temperature", &PROFONT_18_POINT) - .foreground_color(SUB_TEXT_COLOR) - .flex_frame() - .with_infinite_max_height() - .with_vertical_alignment(VerticalAlignment::Bottom) - .with_max_height(25), - Spacer::default().flex_frame().with_max_width(10), + .with_max_height(53), )), // Window - Spacer::default() - .flex_frame() - .with_infinite_max_width() - .with_infinite_max_height(), + Spacer::default(), )) + .with_spacing(2) .with_alignment(HorizontalAlignment::Leading) } + +pub fn header(indicator: MenuIndicatorType, tendency: Tendency) -> impl View { + // Header + HStack::new(( + icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()), + Spacer::default().flex_frame().with_max_width(10), + Spacer::default(), + tendency_indicator(tendency), + Text::new(indicator.get_value_str(), &PROFONT_24_POINT).foreground_color(MAIN_TEXT_COLOR), + Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT) + .foreground_color(SUB_TEXT_COLOR) + .flex_frame() + .with_infinite_max_height() + .with_vertical_alignment(VerticalAlignment::Bottom) + .with_max_height(25), + Spacer::default(), + )) +} diff --git a/src/views/icon.rs b/src/views/icon.rs index 8d0f5c1..6fc616b 100644 --- a/src/views/icon.rs +++ b/src/views/icon.rs @@ -4,12 +4,15 @@ use buoyant::view::ZStack; use buoyant::view::shape::Rectangle; use embedded_graphics::pixelcolor::Rgb565; +use crate::BACKGROUND_COLOR; use crate::get_image; use crate::images::StaticImage; pub fn icon_box_view(box_color: Rgb565, icon: &'static StaticImage) -> impl View { ZStack::new(( - Rectangle.corner_radius(10).foreground_color(box_color), + Rectangle + .corner_radius(10) + .foreground_color(BACKGROUND_COLOR), buoyant::view::Image::new(get_image!(icon)), )) .flex_frame() diff --git a/src/views/menu.rs b/src/views/menu.rs index 23d7a7a..c30469a 100644 --- a/src/views/menu.rs +++ b/src/views/menu.rs @@ -23,19 +23,19 @@ pub fn menu_view() -> impl View { main_menu_indicator(MenuIndicatorType::Temperature(31.5), Tendency::Falling), main_menu_indicator(MenuIndicatorType::Humidity(36.2), Tendency::Steady), )) - .with_spacing(5), + .with_spacing(2), HStack::new(( main_menu_indicator(MenuIndicatorType::Co2(1329), Tendency::Rising), main_menu_indicator(MenuIndicatorType::Voc(29), Tendency::Falling), )) - .with_spacing(5), + .with_spacing(2), )) - .with_spacing(5) + .with_spacing(2) } fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) -> impl View { Rectangle - .corner_radius(10) + .corner_radius(5) .stroked(FRAME_STROKE) .foreground_color(FRAME_STROKE_COLOR) .background(Alignment::Center, || {