diff --git a/egui_demo_lib/src/apps/demo/dancing_strings.rs b/egui_demo_lib/src/apps/demo/dancing_strings.rs index 2d7245cc..e277d196 100644 --- a/egui_demo_lib/src/apps/demo/dancing_strings.rs +++ b/egui_demo_lib/src/apps/demo/dancing_strings.rs @@ -34,6 +34,9 @@ impl super::View for DancingStrings { let desired_size = ui.available_width() * vec2(1.0, 0.35); let (_id, rect) = ui.allocate_space(desired_size); + let to_screen = + math::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect); + let mut shapes = vec![]; for &mode in &[2, 3, 5] { @@ -46,11 +49,7 @@ impl super::View for DancingStrings { let t = i as f32 / (n as f32); let amp = (time as f32 * speed * mode).sin() / mode; let y = amp * (t * std::f32::consts::TAU / 2.0 * mode).sin(); - - pos2( - lerp(rect.x_range(), t), - remap(y, -1.0..=1.0, rect.y_range()), - ) + to_screen * pos2(t, y) }) .collect(); diff --git a/egui_demo_lib/src/apps/demo/painting.rs b/egui_demo_lib/src/apps/demo/painting.rs index 24b90d33..483bd9cc 100644 --- a/egui_demo_lib/src/apps/demo/painting.rs +++ b/egui_demo_lib/src/apps/demo/painting.rs @@ -3,7 +3,8 @@ use egui::*; #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", serde(default))] pub struct Painting { - lines: Vec>, + /// in 0-1 normalized coordinates + lines: Vec>, stroke: Stroke, } @@ -32,6 +33,12 @@ impl Painting { ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::drag()); let rect = response.rect; + let to_screen = emath::RectTransform::from_to( + Rect::from_min_size(Pos2::ZERO, rect.square_proportions()), + rect, + ); + let from_screen = to_screen.inverse(); + if self.lines.is_empty() { self.lines.push(vec![]); } @@ -39,7 +46,7 @@ impl Painting { let current_line = self.lines.last_mut().unwrap(); if let Some(pointer_pos) = response.interact_pointer_pos() { - let canvas_pos = pointer_pos - rect.min; + let canvas_pos = from_screen * pointer_pos; if current_line.last() != Some(&canvas_pos) { current_line.push(canvas_pos); } @@ -49,7 +56,7 @@ impl Painting { for line in &self.lines { if line.len() >= 2 { - let points: Vec = line.iter().map(|p| rect.min + *p).collect(); + let points: Vec = line.iter().map(|p| to_screen * *p).collect(); painter.add(Shape::line(points, self.stroke)); } } diff --git a/egui_demo_lib/src/apps/fractal_clock.rs b/egui_demo_lib/src/apps/fractal_clock.rs index 54c7f554..a174e7e2 100644 --- a/egui_demo_lib/src/apps/fractal_clock.rs +++ b/egui_demo_lib/src/apps/fractal_clock.rs @@ -100,8 +100,6 @@ impl FractalClock { } fn paint(&mut self, painter: &Painter) { - let rect = painter.clip_rect(); - struct Hand { length: f32, angle: f32, @@ -130,14 +128,18 @@ impl FractalClock { Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)), ]; - let scale = self.zoom * rect.width().min(rect.height()); let mut shapes: Vec = Vec::new(); - let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| { - let line = [ - rect.center() + scale * points[0].to_vec2(), - rect.center() + scale * points[1].to_vec2(), - ]; + let rect = painter.clip_rect(); + let to_screen = emath::RectTransform::from_to( + Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom), + rect, + ); + + let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| { + let line = [to_screen * points[0], to_screen * points[1]]; + + // culling if rect.intersects(Rect::from_two_pos(line[0], line[1])) { shapes.push(Shape::line_segment(line, (width, color))); } @@ -186,6 +188,9 @@ impl FractalClock { width *= self.width_factor; let luminance_u8 = (255.0 * luminance).round() as u8; + if luminance_u8 == 0 { + break; + } for &rotor in &hand_rotors { for a in &nodes { diff --git a/egui_demo_lib/src/frame_history.rs b/egui_demo_lib/src/frame_history.rs index 0939e0cf..5d8748ef 100644 --- a/egui_demo_lib/src/frame_history.rs +++ b/egui_demo_lib/src/frame_history.rs @@ -58,7 +58,6 @@ impl FrameHistory { fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response { use egui::*; - let graph_top_cpu_usage = 0.010; ui.label("egui CPU usage history"); let history = &self.frame_times; @@ -69,12 +68,17 @@ impl FrameHistory { let (rect, response) = ui.allocate_at_least(size, Sense::hover()); let style = ui.style().noninteractive(); - let mut shapes = vec![Shape::Rect { + let graph_top_cpu_usage = 0.010; + let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0); + let to_screen = emath::RectTransform::from_to(graph_rect, rect); + + let mut shapes = Vec::with_capacity(3 + 2 * history.len()); + shapes.push(Shape::Rect { rect, corner_radius: style.corner_radius, fill: ui.visuals().extreme_bg_color, stroke: ui.style().noninteractive().bg_stroke, - }]; + }); let rect = rect.shrink(4.0); let color = ui.visuals().text_color(); @@ -87,7 +91,7 @@ impl FrameHistory { [pos2(rect.left(), y), pos2(rect.right(), y)], line_stroke, )); - let cpu_usage = remap(y, rect.bottom_up_range(), 0.0..=graph_top_cpu_usage); + let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; let text = format!("{:.1} ms", 1e3 * cpu_usage); shapes.push(Shape::text( ui.fonts(), @@ -106,16 +110,15 @@ impl FrameHistory { for (time, cpu_usage) in history.iter() { let age = (right_side_time - time) as f32; - let x = remap(age, history.max_age()..=0.0, rect.x_range()); - let y = remap_clamp(cpu_usage, 0.0..=graph_top_cpu_usage, rect.bottom_up_range()); + let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage)); shapes.push(Shape::line_segment( - [pos2(x, rect.bottom()), pos2(x, y)], + [pos2(pos.x, rect.bottom()), pos], line_stroke, )); if cpu_usage < graph_top_cpu_usage { - shapes.push(Shape::circle_filled(pos2(x, y), radius, circle_color)); + shapes.push(Shape::circle_filled(pos, radius, circle_color)); } } diff --git a/emath/src/lib.rs b/emath/src/lib.rs index 37742d91..23f121b0 100644 --- a/emath/src/lib.rs +++ b/emath/src/lib.rs @@ -60,6 +60,7 @@ use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; pub mod align; mod pos2; mod rect; +mod rect_transform; mod rot2; pub mod smart_aim; mod vec2; @@ -68,6 +69,7 @@ pub use { align::{Align, Align2}, pos2::*, rect::*, + rect_transform::*, rot2::*, vec2::*, }; diff --git a/emath/src/rect.rs b/emath/src/rect.rs index e98555ff..fe4fc6f4 100644 --- a/emath/src/rect.rs +++ b/emath/src/rect.rs @@ -202,6 +202,29 @@ impl Rect { pub fn height(&self) -> f32 { self.max.y - self.min.y } + + /// Width / height + /// + /// * `aspect_ratio < 1`: portrait / high + /// * `aspect_ratio = 1`: square + /// * `aspect_ratio > 1`: landscape / wide + pub fn aspect_ratio(&self) -> f32 { + self.width() / self.height() + } + + /// `[2, 1]` for wide screen, and `[1, 2]` for portrait, etc. + /// At least one dimension = 1, the other >= 1 + /// Returns the proportions required to letter-box a square view area. + pub fn square_proportions(&self) -> Vec2 { + let w = self.width(); + let h = self.height(); + if w > h { + vec2(w / h, 1.0) + } else { + vec2(1.0, h / w) + } + } + pub fn area(&self) -> f32 { self.width() * self.height() } diff --git a/emath/src/rect_transform.rs b/emath/src/rect_transform.rs new file mode 100644 index 00000000..e758652c --- /dev/null +++ b/emath/src/rect_transform.rs @@ -0,0 +1,73 @@ +use crate::*; + +/// Linearly transforms positions from one [`Rect`] to another. +/// +/// `RectTransform` stores the rectangles, and therefore supports clamping and culling. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RectTransform { + from: Rect, + to: Rect, +} + +impl RectTransform { + pub fn identity(from_and_to: Rect) -> Self { + Self::from_to(from_and_to, from_and_to) + } + + pub fn from_to(from: Rect, to: Rect) -> Self { + Self { from, to } + } + + pub fn from(&self) -> &Rect { + &self.from + } + + pub fn to(&self) -> &Rect { + &self.to + } + + pub fn inverse(&self) -> RectTransform { + Self::from_to(self.to, self.from) + } + + /// Transforms the given coordinate in the `from` space to the `to` space. + pub fn transform_pos(&self, pos: Pos2) -> Pos2 { + pos2( + remap(pos.x, self.from.x_range(), self.to.x_range()), + remap(pos.y, self.from.y_range(), self.to.y_range()), + ) + } + + /// Transforms the given rectangle in the `in`-space to a rectangle in the `out`-space. + pub fn transform_rect(&self, rect: Rect) -> Rect { + Rect { + min: self.transform_pos(rect.min), + max: self.transform_pos(rect.max), + } + } + + /// Transforms the given coordinate in the `from` space to the `to` space, + /// clamping if necessary. + pub fn transform_pos_clamped(&self, pos: Pos2) -> Pos2 { + pos2( + remap_clamp(pos.x, self.from.x_range(), self.to.x_range()), + remap_clamp(pos.y, self.from.y_range(), self.to.y_range()), + ) + } +} + +/// Transforms the position. +impl std::ops::Mul for RectTransform { + type Output = Pos2; + fn mul(self, pos: Pos2) -> Pos2 { + self.transform_pos(pos) + } +} + +/// Transforms the position. +impl std::ops::Mul for &RectTransform { + type Output = Pos2; + fn mul(self, pos: Pos2) -> Pos2 { + self.transform_pos(pos) + } +} diff --git a/emath/src/rot2.rs b/emath/src/rot2.rs index 2ec1783d..e274bd85 100644 --- a/emath/src/rot2.rs +++ b/emath/src/rot2.rs @@ -8,7 +8,9 @@ use super::Vec2; // `vec2(c,s)` represents where the X axis will end up after rotation. // /// Represents a rotation in the 2D plane. +// /// A rotation of 𝞃/4 = 90° rotates the X axis to the Y axis. +// /// Normally a `Rot2` is normalized (unit-length). /// If not, it will also scale vectors. #[derive(Clone, Copy, PartialEq)] @@ -104,6 +106,7 @@ impl std::ops::Mul for Rot2 { } } +/// Rotates (and maybe scales) the vector. impl std::ops::Mul for Rot2 { type Output = Vec2; fn mul(self, v: Vec2) -> Vec2 { @@ -114,6 +117,7 @@ impl std::ops::Mul for Rot2 { } } +/// Scales the rotor. impl std::ops::Mul for f32 { type Output = Rot2; fn mul(self, r: Rot2) -> Rot2 { @@ -124,6 +128,7 @@ impl std::ops::Mul for f32 { } } +/// Scales the rotor. impl std::ops::Mul for Rot2 { type Output = Rot2; fn mul(self, r: f32) -> Rot2 { @@ -134,6 +139,7 @@ impl std::ops::Mul for Rot2 { } } +/// Scales the rotor. impl std::ops::Div for Rot2 { type Output = Rot2; fn div(self, r: f32) -> Rot2 {