From 912ced2622fb3b6c04f14bd45e6d211f66df2fbe Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 12 Apr 2024 16:49:23 -0600 Subject: [PATCH 01/27] added ability to add custom gradients to paths --- crates/egui/src/painter.rs | 12 +-- crates/epaint/src/bezier.rs | 28 +++--- crates/epaint/src/lib.rs | 2 +- crates/epaint/src/shape.rs | 36 ++++--- crates/epaint/src/shape_transform.rs | 48 +++++---- crates/epaint/src/stroke.rs | 83 +++++++++++++++ crates/epaint/src/tessellator.rs | 139 +++++++++++++++++++------- crates/epaint/src/text/text_layout.rs | 4 +- examples/hello_world/src/main.rs | 24 ++++- 9 files changed, 278 insertions(+), 98 deletions(-) diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 788319dc936..9baeb208191 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -7,7 +7,7 @@ use crate::{ }; use epaint::{ text::{Fonts, Galley, LayoutJob}, - CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, + CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke, }; /// Helper to paint shapes and text to a specific region on a specific layer. @@ -280,7 +280,7 @@ impl Painter { /// # Paint different primitives impl Painter { /// Paints a line from the first point to the second. - pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into) -> ShapeIdx { + pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into) -> ShapeIdx { self.add(Shape::LineSegment { points, stroke: stroke.into(), @@ -288,13 +288,13 @@ impl Painter { } /// Paints a horizontal line. - pub fn hline(&self, x: impl Into, y: f32, stroke: impl Into) -> ShapeIdx { - self.add(Shape::hline(x, y, stroke)) + pub fn hline(&self, x: impl Into, y: f32, stroke: impl Into) -> ShapeIdx { + self.add(Shape::hline(x, y, stroke.into())) } /// Paints a vertical line. - pub fn vline(&self, x: f32, y: impl Into, stroke: impl Into) -> ShapeIdx { - self.add(Shape::vline(x, y, stroke)) + pub fn vline(&self, x: f32, y: impl Into, stroke: impl Into) -> ShapeIdx { + self.add(Shape::vline(x, y, stroke.into())) } pub fn circle( diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/bezier.rs index 3da99f33b65..4ad9a28228f 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/bezier.rs @@ -3,7 +3,7 @@ use std::ops::Range; -use crate::{shape::Shape, Color32, PathShape, Stroke}; +use crate::{shape::Shape, Color32, PathShape, PathStroke}; use emath::*; // ---------------------------------------------------------------------------- @@ -11,7 +11,7 @@ use emath::*; /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// /// See also [`QuadraticBezierShape`]. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct CubicBezierShape { /// The first point is the starting point and the last one is the ending point of the curve. @@ -20,7 +20,7 @@ pub struct CubicBezierShape { pub closed: bool, pub fill: Color32, - pub stroke: Stroke, + pub stroke: PathStroke, } impl CubicBezierShape { @@ -32,7 +32,7 @@ impl CubicBezierShape { points: [Pos2; 4], closed: bool, fill: Color32, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, @@ -52,7 +52,7 @@ impl CubicBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -69,7 +69,7 @@ impl CubicBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), }; pathshapes.push(pathshape); } @@ -156,7 +156,7 @@ impl CubicBezierShape { points: [d_from, d_ctrl, d_to], closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), }; let delta_t = t_range.end - t_range.start; let q_start = q.sample(t_range.start); @@ -168,7 +168,7 @@ impl CubicBezierShape { points: [from, ctrl1, ctrl2, to], closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -375,7 +375,7 @@ impl From for Shape { /// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// /// See also [`CubicBezierShape`]. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct QuadraticBezierShape { /// The first point is the starting point and the last one is the ending point of the curve. @@ -384,7 +384,7 @@ pub struct QuadraticBezierShape { pub closed: bool, pub fill: Color32, - pub stroke: Stroke, + pub stroke: PathStroke, } impl QuadraticBezierShape { @@ -397,7 +397,7 @@ impl QuadraticBezierShape { points: [Pos2; 3], closed: bool, fill: Color32, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, @@ -417,7 +417,7 @@ impl QuadraticBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -429,7 +429,7 @@ impl QuadraticBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -688,7 +688,7 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape points: [curve.points[0], c, curve.points[3]], closed: curve.closed, fill: curve.fill, - stroke: curve.stroke, + stroke: curve.stroke.clone(), } } diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 18298484679..c21f41bd108 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -53,7 +53,7 @@ pub use self::{ Rounding, Shape, TextShape, }, stats::PaintStats, - stroke::Stroke, + stroke::{ColorMode, PathStroke, Stroke}, tessellator::{TessellationOptions, Tessellator}, text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 7922a92d6d5..07a9251fad1 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -3,6 +3,7 @@ use std::{any::Any, sync::Arc}; use crate::{ + stroke::PathStroke, text::{FontId, Fonts, Galley}, Color32, Mesh, Stroke, TextureId, }; @@ -34,7 +35,10 @@ pub enum Shape { Ellipse(EllipseShape), /// A line between two points. - LineSegment { points: [Pos2; 2], stroke: Stroke }, + LineSegment { + points: [Pos2; 2], + stroke: PathStroke, + }, /// A series of lines between points. /// The path can have a stroke and/or fill (if closed). @@ -63,11 +67,11 @@ pub enum Shape { Callback(PaintCallback), } -#[test] -fn shape_impl_send_sync() { - fn assert_send_sync() {} - assert_send_sync::(); -} +// #[test] +// fn shape_impl_send_sync() { +// fn assert_send_sync() {} +// assert_send_sync::(); +// } impl From> for Shape { #[inline(always)] @@ -88,7 +92,7 @@ impl Shape { /// A line between two points. /// More efficient than calling [`Self::line`]. #[inline] - pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { + pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { Self::LineSegment { points, stroke: stroke.into(), @@ -96,7 +100,7 @@ impl Shape { } /// A horizontal line. - pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { + pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { let x = x.into(); Self::LineSegment { points: [pos2(x.min, y), pos2(x.max, y)], @@ -105,7 +109,7 @@ impl Shape { } /// A vertical line. - pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { + pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { let y = y.into(); Self::LineSegment { points: [pos2(x, y.min), pos2(x, y.max)], @@ -117,13 +121,13 @@ impl Shape { /// /// Use [`Self::line_segment`] instead if your line only connects two points. #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { + pub fn line(points: Vec, stroke: impl Into) -> Self { Self::Path(PathShape::line(points, stroke)) } /// A line that closes back to the start point again. #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { Self::Path(PathShape::closed_line(points, stroke)) } @@ -224,7 +228,7 @@ impl Shape { pub fn convex_polygon( points: Vec, fill: impl Into, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self::Path(PathShape::convex_polygon(points, fill, stroke)) } @@ -586,7 +590,7 @@ pub struct PathShape { pub fill: Color32, /// Color and thickness of the line. - pub stroke: Stroke, + pub stroke: PathStroke, // TODO(emilk): Add texture support either by supplying uv for each point, // or by some transform from points to uv (e.g. a callback or a linear transform matrix). } @@ -596,7 +600,7 @@ impl PathShape { /// /// Use [`Shape::line_segment`] instead if your line only connects two points. #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { + pub fn line(points: Vec, stroke: impl Into) -> Self { Self { points, closed: false, @@ -607,7 +611,7 @@ impl PathShape { /// A line that closes back to the start point again. #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { Self { points, closed: true, @@ -623,7 +627,7 @@ impl PathShape { pub fn convex_polygon( points: Vec, fill: impl Into, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 8ff65d2a045..296f5d9f483 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -10,8 +10,34 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_colors(shape, adjust_color); } } - Shape::LineSegment { stroke, points: _ } => { - adjust_color(&mut stroke.color); + Shape::LineSegment { stroke, points: _ } => match stroke.color { + stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), + stroke::ColorMode::UV(_) => {} + }, + + Shape::Path(PathShape { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::QuadraticBezier(QuadraticBezierShape { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::CubicBezier(CubicBezierShape { + points: _, + closed: _, + fill, + stroke, + }) => { + adjust_color(fill); + match stroke.color { + stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), + stroke::ColorMode::UV(_) => {} + } } Shape::Circle(CircleShape { @@ -26,12 +52,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { fill, stroke, }) - | Shape::Path(PathShape { - points: _, - closed: _, - fill, - stroke, - }) | Shape::Rect(RectShape { rect: _, rounding: _, @@ -40,18 +60,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { blur_width: _, fill_texture_id: _, uv: _, - }) - | Shape::QuadraticBezier(QuadraticBezierShape { - points: _, - closed: _, - fill, - stroke, - }) - | Shape::CubicBezier(CubicBezierShape { - points: _, - closed: _, - fill, - stroke, }) => { adjust_color(fill); adjust_color(&mut stroke.color); diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 15ddd231f1b..7b4b865c548 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -1,5 +1,7 @@ #![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine +use std::{fmt::Debug, sync::Arc}; + use super::*; /// Describes the width and color of a line. @@ -52,3 +54,84 @@ impl std::hash::Hash for Stroke { color.hash(state); } } + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ColorMode { + Solid(Color32), + #[cfg_attr(feature = "serde", serde(skip))] + UV(Arc Color32 + Send + Sync>>), +} + +impl Default for ColorMode { + fn default() -> Self { + Self::Solid(Color32::TRANSPARENT) + } +} + +impl Debug for ColorMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), + Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), + } + } +} + +impl PartialEq for ColorMode { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Solid(l0), Self::Solid(r0)) => l0 == r0, + (Self::UV(_l0), Self::UV(_r0)) => false, + _ => false, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PathStroke { + pub width: f32, + pub color: ColorMode, +} + +impl PathStroke { + /// Same as [`Stroke::default`]. + pub const NONE: Self = Self { + width: 0.0, + color: ColorMode::Solid(Color32::TRANSPARENT), + }; + + #[inline] + pub fn new(width: impl Into, color: impl Into) -> Self { + Self { + width: width.into(), + color: ColorMode::Solid(color.into()), + } + } + + /// True if width is zero or color is transparent + #[inline] + pub fn is_empty(&self) -> bool { + self.width <= 0.0 || self.color == ColorMode::Solid(Color32::TRANSPARENT) + } +} + +impl From<(f32, Color)> for PathStroke +where + Color: Into, +{ + #[inline(always)] + fn from((width, color): (f32, Color)) -> Self { + Self::new(width, color) + } +} + +impl From for PathStroke { + fn from(value: Stroke) -> Self { + Self { + width: value.width, + color: ColorMode::Solid(value.color), + } + } +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f5b3d9e3cb1..ff61f20c2f5 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,6 +9,11 @@ use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; +use self::{ + stroke::{ColorMode, PathStroke}, + util::OrderedFloat, +}; + // ---------------------------------------------------------------------------- #[allow(clippy::approx_constant)] @@ -471,16 +476,22 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &self.0, PathType::Closed, stroke, out); } - pub fn stroke(&self, feathering: f32, path_type: PathType, stroke: Stroke, out: &mut Mesh) { + pub fn stroke( + &self, + feathering: f32, + path_type: PathType, + stroke: &PathStroke, + out: &mut Mesh, + ) { stroke_path(feathering, &self.0, path_type, stroke, out); } @@ -864,19 +875,53 @@ fn stroke_path( feathering: f32, path: &[PathPoint], path_type: PathType, - stroke: Stroke, + stroke: &PathStroke, out: &mut Mesh, ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == Color32::TRANSPARENT || n < 2 { + if stroke.width <= 0.0 || stroke.color == ColorMode::Solid(Color32::TRANSPARENT) || n < 2 { return; } let idx = out.vertices.len() as u32; + let min_x = path + .iter() + .map(|point| OrderedFloat::from(point.pos.x)) + .min() + .map(OrderedFloat::into_inner) + .unwrap(); + let max_x = path + .iter() + .map(|point| OrderedFloat::from(point.pos.x)) + .max() + .map(OrderedFloat::into_inner) + .unwrap(); + let min_y = path + .iter() + .map(|point| OrderedFloat::from(point.pos.y)) + .min() + .map(OrderedFloat::into_inner) + .unwrap(); + let max_y = path + .iter() + .map(|point| OrderedFloat::from(point.pos.y)) + .max() + .map(OrderedFloat::into_inner) + .unwrap(); + + let get_color = |col: &ColorMode, pos: Pos2| match col { + ColorMode::Solid(col) => *col, + ColorMode::UV(fun) => { + let x = remap_clamp(pos.x, min_x..=max_x, 0.0..=1.0); + let y = remap_clamp(pos.y, min_y..=max_y, 0.0..=1.0); + fun(pos2(x, y)) + } + }; + if feathering > 0.0 { - let color_inner = stroke.color; + let color_inner = &stroke.color; let color_outer = Color32::TRANSPARENT; let thin_line = stroke.width <= feathering; @@ -889,9 +934,11 @@ fn stroke_path( */ // Fade out as it gets thinner: - let color_inner = mul_color(color_inner, stroke.width / feathering); - if color_inner == Color32::TRANSPARENT { - return; + if let ColorMode::Solid(col) = color_inner { + let color_inner = mul_color(*col, stroke.width / feathering); + if color_inner == Color32::TRANSPARENT { + return; + } } out.reserve_triangles(4 * n as usize); @@ -904,7 +951,7 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex(p, color_inner); + out.colored_vertex(p, get_color(color_inner, p)); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { @@ -942,9 +989,10 @@ fn stroke_path( let p1 = &path[i1 as usize]; let p = p1.pos; let n = p1.normal; + let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex(p + n * inner_rad, color); + out.colored_vertex(p - n * inner_rad, color); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -982,9 +1030,10 @@ fn stroke_path( let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; + let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex(p + n * inner_rad, color); + out.colored_vertex(p - n * inner_rad, color); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 0, idx + 1, idx + 2); @@ -996,9 +1045,10 @@ fn stroke_path( let point = &path[i1 as usize]; let p = point.pos; let n = point.normal; + let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex(p + n * inner_rad, color); + out.colored_vertex(p - n * inner_rad, color); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1019,9 +1069,10 @@ fn stroke_path( let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; + let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex(p + n * inner_rad, color); + out.colored_vertex(p - n * inner_rad, color); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1067,19 +1118,22 @@ fn stroke_path( if thin_line { // Fade out thin lines rather than making them thinner let radius = feathering / 2.0; - let color = mul_color(stroke.color, stroke.width / feathering); - if color == Color32::TRANSPARENT { - return; + if let ColorMode::Solid(color) = stroke.color { + let color = mul_color(color, stroke.width / feathering); + if color == Color32::TRANSPARENT { + return; + } } for p in path { + let color = mul_color(get_color(&stroke.color, p.pos), stroke.width / feathering); out.colored_vertex(p.pos + radius * p.normal, color); out.colored_vertex(p.pos - radius * p.normal, color); } } else { let radius = stroke.width / 2.0; for p in path { - out.colored_vertex(p.pos + radius * p.normal, stroke.color); - out.colored_vertex(p.pos - radius * p.normal, stroke.color); + out.colored_vertex(p.pos + radius * p.normal, get_color(&stroke.color, p.pos)); + out.colored_vertex(p.pos - radius * p.normal, get_color(&stroke.color, p.pos)); } } } @@ -1275,9 +1329,9 @@ impl Tessellator { self.tessellate_text(&text_shape, out); } Shape::QuadraticBezier(quadratic_shape) => { - self.tessellate_quadratic_bezier(quadratic_shape, out); + self.tessellate_quadratic_bezier(&quadratic_shape, out); } - Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out), + Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(&cubic_shape, out), Shape::Callback(_) => { panic!("Shape::Callback passed to Tessellator"); } @@ -1337,7 +1391,7 @@ impl Tessellator { self.scratchpad_path.add_circle(center, radius); self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, stroke, out); + .stroke_closed(self.feathering, &stroke.into(), out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1404,7 +1458,7 @@ impl Tessellator { self.scratchpad_path.add_line_loop(&points); self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, stroke, out); + .stroke_closed(self.feathering, &stroke.into(), out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1430,7 +1484,13 @@ impl Tessellator { /// /// * `shape`: the mesh to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_line(&mut self, points: [Pos2; 2], stroke: Stroke, out: &mut Mesh) { + pub fn tessellate_line( + &mut self, + points: [Pos2; 2], + stroke: impl Into, + out: &mut Mesh, + ) { + let stroke = stroke.into(); if stroke.is_empty() { return; } @@ -1446,7 +1506,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_segment(points); self.scratchpad_path - .stroke_open(self.feathering, stroke, out); + .stroke_open(self.feathering, &stroke, out); } /// Tessellate a single [`PathShape`] into a [`Mesh`]. @@ -1493,7 +1553,7 @@ impl Tessellator { PathType::Open }; self.scratchpad_path - .stroke(self.feathering, typ, *stroke, out); + .stroke(self.feathering, typ, stroke, out); } /// Tessellate a single [`Rect`] into a [`Mesh`]. @@ -1588,7 +1648,7 @@ impl Tessellator { path.fill(self.feathering, fill, out); } - path.stroke_closed(self.feathering, stroke, out); + path.stroke_closed(self.feathering, &stroke.into(), out); } self.feathering = old_feathering; // restore @@ -1707,8 +1767,11 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); - self.scratchpad_path - .stroke_open(self.feathering, *underline, out); + self.scratchpad_path.stroke_open( + self.feathering, + &PathStroke::from(*underline), + out, + ); } } } @@ -1719,7 +1782,7 @@ impl Tessellator { /// * `out`: triangles are appended to this. pub fn tessellate_quadratic_bezier( &mut self, - quadratic_shape: QuadraticBezierShape, + quadratic_shape: &QuadraticBezierShape, out: &mut Mesh, ) { let options = &self.options; @@ -1737,7 +1800,7 @@ impl Tessellator { &points, quadratic_shape.fill, quadratic_shape.closed, - quadratic_shape.stroke, + &quadratic_shape.stroke, out, ); } @@ -1746,7 +1809,7 @@ impl Tessellator { /// /// * `cubic_shape`: the shape to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_cubic_bezier(&mut self, cubic_shape: CubicBezierShape, out: &mut Mesh) { + pub fn tessellate_cubic_bezier(&mut self, cubic_shape: &CubicBezierShape, out: &mut Mesh) { let options = &self.options; let clip_rect = self.clip_rect; if options.coarse_tessellation_culling @@ -1763,7 +1826,7 @@ impl Tessellator { &points, cubic_shape.fill, cubic_shape.closed, - cubic_shape.stroke, + &cubic_shape.stroke, out, ); } @@ -1774,7 +1837,7 @@ impl Tessellator { points: &[Pos2], fill: Color32, closed: bool, - stroke: Stroke, + stroke: &PathStroke, out: &mut Mesh, ) { if points.len() < 2 { diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 322026da3c5..59ecd8081d5 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::*; -use crate::{text::font::Font, Color32, Mesh, Stroke, Vertex}; +use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; @@ -853,7 +853,7 @@ fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations. path.add_line_segment([start, stop]); let feathering = 1.0 / point_scale.pixels_per_point(); - path.stroke_open(feathering, stroke, mesh); + path.stroke_open(feathering, &PathStroke::from(stroke), mesh); } else { // Thin lines often lost, so this is a bad idea diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index b3fda5a5810..74a6aad7cae 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -1,6 +1,11 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -use eframe::egui; +use std::sync::Arc; + +use eframe::{ + egui::{self, vec2, Color32, Pos2, Sense, Shape}, + epaint::PathStroke, +}; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -52,6 +57,23 @@ impl eframe::App for MyApp { ui.image(egui::include_image!( "../../../crates/egui/assets/ferris.png" )); + + let (rect, _) = + ui.allocate_exact_size(vec2(100.0, 50.0), Sense::focusable_noninteractive()); + + ui.painter_at(rect).add(Shape::line_segment( + [rect.left_center(), rect.right_center()], + PathStroke { + width: 1.5, + color: egui::epaint::ColorMode::UV(Arc::new(Box::new(|p: Pos2| { + if p.x < 0.5 { + Color32::RED + } else { + Color32::GREEN + } + }))), + }, + )) }); } } From 18c561dc8fd67f79eb488689b0fe6db60f375e48 Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 12 Apr 2024 16:54:15 -0600 Subject: [PATCH 02/27] add docs --- crates/epaint/src/stroke.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 7b4b865c548..a688e5c2554 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,10 +55,15 @@ impl std::hash::Hash for Stroke { } } +/// How paths will be colored. #[derive(Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ColorMode { + /// The entire path is one solid color, this is the default. Solid(Color32), + /// Provide a callback which takes in a UV coordinate and converts it to a color. The values passed to this will always be one. + /// + /// # This cannot be serialized #[cfg_attr(feature = "serde", serde(skip))] UV(Arc Color32 + Send + Sync>>), } @@ -96,7 +101,7 @@ pub struct PathStroke { } impl PathStroke { - /// Same as [`Stroke::default`]. + /// Same as [`PathStroke::default`]. pub const NONE: Self = Self { width: 0.0, color: ColorMode::Solid(Color32::TRANSPARENT), @@ -110,6 +115,17 @@ impl PathStroke { } } + #[inline] + pub fn new_uv( + width: impl Into, + callback: impl Fn(Pos2) -> Color32 + Send + Sync + 'static, + ) -> Self { + Self { + width: width.into(), + color: ColorMode::UV(Arc::new(Box::new(callback))), + } + } + /// True if width is zero or color is transparent #[inline] pub fn is_empty(&self) -> bool { From 42798510ded8385d73ab49e33dc09cae0232c96b Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 12 Apr 2024 16:59:40 -0600 Subject: [PATCH 03/27] re-add send sync assertion --- crates/epaint/src/shape.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 07a9251fad1..336c9087316 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -67,11 +67,11 @@ pub enum Shape { Callback(PaintCallback), } -// #[test] -// fn shape_impl_send_sync() { -// fn assert_send_sync() {} -// assert_send_sync::(); -// } +#[test] +fn shape_impl_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); +} impl From> for Shape { #[inline(always)] From fe92ba8c80505433667352deb3a19175515a209f Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 12 Apr 2024 19:21:30 -0600 Subject: [PATCH 04/27] better documentation --- crates/epaint/src/stroke.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index a688e5c2554..c5368e15d1a 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -61,9 +61,10 @@ impl std::hash::Hash for Stroke { pub enum ColorMode { /// The entire path is one solid color, this is the default. Solid(Color32), + /// Provide a callback which takes in a UV coordinate and converts it to a color. The values passed to this will always be one. /// - /// # This cannot be serialized + /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] UV(Arc Color32 + Send + Sync>>), } @@ -93,6 +94,9 @@ impl PartialEq for ColorMode { } } +/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] +/// +/// The default stroke is the same as [`Stroke::NONE`]. #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PathStroke { @@ -126,7 +130,7 @@ impl PathStroke { } } - /// True if width is zero or color is transparent + /// True if width is zero or color is solid and transparent #[inline] pub fn is_empty(&self) -> bool { self.width <= 0.0 || self.color == ColorMode::Solid(Color32::TRANSPARENT) From c433de35e2f7534a43f02fa1fc31e9e14daa046d Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 12 Apr 2024 19:24:57 -0600 Subject: [PATCH 05/27] fix typo in docs --- crates/epaint/src/stroke.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index c5368e15d1a..39171fef803 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -62,7 +62,7 @@ pub enum ColorMode { /// The entire path is one solid color, this is the default. Solid(Color32), - /// Provide a callback which takes in a UV coordinate and converts it to a color. The values passed to this will always be one. + /// Provide a callback which takes in a UV coordinate and converts it to a color. The values passed to this will always be between zero and one. /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] From e03fe525a979558699a7bfdfa55e6f77578043c0 Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 13 Apr 2024 11:13:22 -0600 Subject: [PATCH 06/27] added rect bounds for uv mapping --- crates/epaint/src/shape_transform.rs | 2 ++ crates/epaint/src/stroke.rs | 14 +++++++++++++- crates/epaint/src/tessellator.rs | 5 +++++ examples/hello_world/src/main.rs | 29 ++++++++++++++++++++-------- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 296f5d9f483..5afdd735d81 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -13,6 +13,7 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { Shape::LineSegment { stroke, points: _ } => match stroke.color { stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), stroke::ColorMode::UV(_) => {} + stroke::ColorMode::UVBounds(_, _) => {} }, Shape::Path(PathShape { @@ -37,6 +38,7 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { match stroke.color { stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), stroke::ColorMode::UV(_) => {} + stroke::ColorMode::UVBounds(_, _) => {} } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 39171fef803..56233ba3833 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -62,11 +62,18 @@ pub enum ColorMode { /// The entire path is one solid color, this is the default. Solid(Color32), - /// Provide a callback which takes in a UV coordinate and converts it to a color. The values passed to this will always be between zero and one. + /// Provide a callback which takes in a UV coordinate and converts it to a color. The UVs are relative to the bounding box of the path. + /// If you need something more custom, consider [`ColorMode::UVBounds`] /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] UV(Arc Color32 + Send + Sync>>), + + /// Provide a callback which takes in a UV coordinate and converts it to a color. The UVs are relative to the Rect provided. + /// + /// **This cannot be serialized** + #[cfg_attr(feature = "serde", serde(skip))] + UVBounds(Rect, Arc Color32 + Send + Sync>>), } impl Default for ColorMode { @@ -80,6 +87,11 @@ impl Debug for ColorMode { match self { Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), + Self::UVBounds(rect, _) => f + .debug_tuple("UV (custom rect)") + .field(rect) + .field(&"") + .finish(), } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index ff61f20c2f5..27a3eff87c6 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -918,6 +918,11 @@ fn stroke_path( let y = remap_clamp(pos.y, min_y..=max_y, 0.0..=1.0); fun(pos2(x, y)) } + ColorMode::UVBounds(rect, fun) => { + let x = remap_clamp(pos.x, rect.min.x..=rect.max.x, 0.0..=1.0); + let y = remap_clamp(pos.y, rect.min.y..=rect.max.y, 0.0..=1.0); + fun(pos2(x, y)) + } }; if feathering > 0.0 { diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 74a6aad7cae..81afa2fe79a 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -61,19 +61,32 @@ impl eframe::App for MyApp { let (rect, _) = ui.allocate_exact_size(vec2(100.0, 50.0), Sense::focusable_noninteractive()); - ui.painter_at(rect).add(Shape::line_segment( - [rect.left_center(), rect.right_center()], - PathStroke { - width: 1.5, - color: egui::epaint::ColorMode::UV(Arc::new(Box::new(|p: Pos2| { + let stroke = PathStroke { + width: 1.5, + color: egui::epaint::ColorMode::UVBounds( + rect, + Arc::new(Box::new(|p: Pos2| { if p.x < 0.5 { Color32::RED } else { Color32::GREEN } - }))), - }, - )) + })), + ), + }; + + ui.painter_at(rect).add(Shape::line_segment( + [rect.left_center(), rect.right_center()], + stroke.clone(), + )); + + ui.painter_at(rect).add(Shape::line_segment( + [ + rect.left_center() + vec2(25.0, 15.0), + rect.right_center() + vec2(0.0, 15.0), + ], + stroke.clone(), + )); }); } } From c9efc240d03842e16716a92d1077d84ee06e7543 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 09:11:52 -0600 Subject: [PATCH 07/27] added benchmarks for colors --- crates/epaint/benches/benchmark.rs | 36 ++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 709adbfae9c..c27aa83f9e5 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use epaint::*; +use epaint::{tessellator::Path, *}; fn single_dashed_lines(c: &mut Criterion) { c.bench_function("single_dashed_lines", move |b| { @@ -72,10 +72,42 @@ fn tessellate_circles(c: &mut Criterion) { }); } +fn thick_line_solid(c: &mut Criterion) { + c.bench_function("thick_solid_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thin_line_solid(c: &mut Criterion) { + c.bench_function("thin_solid_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + criterion_group!( benches, single_dashed_lines, many_dashed_lines, - tessellate_circles + tessellate_circles, + thick_line_solid, + thin_line_solid ); criterion_main!(benches); From 66644820a93a76758da3d8f8f5d7b03ba88f79a9 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 09:29:03 -0600 Subject: [PATCH 08/27] simplify ColorMode and use a better method for calculating bounding boxes --- crates/epaint/src/shape_transform.rs | 2 -- crates/epaint/src/stroke.rs | 18 ++---------- crates/epaint/src/tessellator.rs | 41 ++-------------------------- examples/hello_world/src/main.rs | 20 ++++++-------- 4 files changed, 15 insertions(+), 66 deletions(-) diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 5afdd735d81..296f5d9f483 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -13,7 +13,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { Shape::LineSegment { stroke, points: _ } => match stroke.color { stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), stroke::ColorMode::UV(_) => {} - stroke::ColorMode::UVBounds(_, _) => {} }, Shape::Path(PathShape { @@ -38,7 +37,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { match stroke.color { stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), stroke::ColorMode::UV(_) => {} - stroke::ColorMode::UVBounds(_, _) => {} } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 56233ba3833..dda1f646564 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -62,18 +62,11 @@ pub enum ColorMode { /// The entire path is one solid color, this is the default. Solid(Color32), - /// Provide a callback which takes in a UV coordinate and converts it to a color. The UVs are relative to the bounding box of the path. - /// If you need something more custom, consider [`ColorMode::UVBounds`] + /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] - UV(Arc Color32 + Send + Sync>>), - - /// Provide a callback which takes in a UV coordinate and converts it to a color. The UVs are relative to the Rect provided. - /// - /// **This cannot be serialized** - #[cfg_attr(feature = "serde", serde(skip))] - UVBounds(Rect, Arc Color32 + Send + Sync>>), + UV(Arc Color32 + Send + Sync>>), } impl Default for ColorMode { @@ -87,11 +80,6 @@ impl Debug for ColorMode { match self { Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), - Self::UVBounds(rect, _) => f - .debug_tuple("UV (custom rect)") - .field(rect) - .field(&"") - .finish(), } } } @@ -134,7 +122,7 @@ impl PathStroke { #[inline] pub fn new_uv( width: impl Into, - callback: impl Fn(Pos2) -> Color32 + Send + Sync + 'static, + callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static, ) -> Self { Self { width: width.into(), diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 27a3eff87c6..82f99d8e386 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,10 +9,7 @@ use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; -use self::{ - stroke::{ColorMode, PathStroke}, - util::OrderedFloat, -}; +use self::stroke::{ColorMode, PathStroke}; // ---------------------------------------------------------------------------- @@ -886,43 +883,11 @@ fn stroke_path( let idx = out.vertices.len() as u32; - let min_x = path - .iter() - .map(|point| OrderedFloat::from(point.pos.x)) - .min() - .map(OrderedFloat::into_inner) - .unwrap(); - let max_x = path - .iter() - .map(|point| OrderedFloat::from(point.pos.x)) - .max() - .map(OrderedFloat::into_inner) - .unwrap(); - let min_y = path - .iter() - .map(|point| OrderedFloat::from(point.pos.y)) - .min() - .map(OrderedFloat::into_inner) - .unwrap(); - let max_y = path - .iter() - .map(|point| OrderedFloat::from(point.pos.y)) - .max() - .map(OrderedFloat::into_inner) - .unwrap(); + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, - ColorMode::UV(fun) => { - let x = remap_clamp(pos.x, min_x..=max_x, 0.0..=1.0); - let y = remap_clamp(pos.y, min_y..=max_y, 0.0..=1.0); - fun(pos2(x, y)) - } - ColorMode::UVBounds(rect, fun) => { - let x = remap_clamp(pos.x, rect.min.x..=rect.max.x, 0.0..=1.0); - let y = remap_clamp(pos.y, rect.min.y..=rect.max.y, 0.0..=1.0); - fun(pos2(x, y)) - } + ColorMode::UV(fun) => fun(bbox, pos), }; if feathering > 0.0 { diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 81afa2fe79a..a72d8b25cb6 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use eframe::{ - egui::{self, vec2, Color32, Pos2, Sense, Shape}, + egui::{self, remap_clamp, vec2, Color32, Sense, Shape}, epaint::PathStroke, }; @@ -63,16 +63,14 @@ impl eframe::App for MyApp { let stroke = PathStroke { width: 1.5, - color: egui::epaint::ColorMode::UVBounds( - rect, - Arc::new(Box::new(|p: Pos2| { - if p.x < 0.5 { - Color32::RED - } else { - Color32::GREEN - } - })), - ), + color: egui::epaint::ColorMode::UV(Arc::new(Box::new(|r, p| { + let t = remap_clamp(p.x, r.x_range(), 0.0..=1.0); + if t < 0.5 { + Color32::RED + } else { + Color32::GREEN + } + }))), }; ui.painter_at(rect).add(Shape::line_segment( From 5451e3043796568c4bce0895d01cfd118e2d1f01 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 09:32:02 -0600 Subject: [PATCH 09/27] fix feathering oversight --- crates/epaint/src/tessellator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 82f99d8e386..0e8ac0e7d5b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -921,7 +921,10 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex(p, get_color(color_inner, p)); + out.colored_vertex( + p, + mul_color(get_color(color_inner, p), stroke.width / feathering), + ); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { From 41f819214bfac6bb9da35667ff4a91a0bf2cc5c6 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 09:51:55 -0600 Subject: [PATCH 10/27] refactor ColorMode out into its own file --- crates/epaint/src/color.rs | 43 ++++++++++++++++++++++++++++ crates/epaint/src/lib.rs | 4 ++- crates/epaint/src/shape_transform.rs | 8 +++--- crates/epaint/src/stroke.rs | 39 ------------------------- crates/epaint/src/tessellator.rs | 3 +- 5 files changed, 52 insertions(+), 45 deletions(-) create mode 100644 crates/epaint/src/color.rs diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs new file mode 100644 index 00000000000..5da0a152b26 --- /dev/null +++ b/crates/epaint/src/color.rs @@ -0,0 +1,43 @@ +use std::{fmt::Debug, sync::Arc}; + +use ecolor::Color32; +use emath::{Pos2, Rect}; + +/// How paths will be colored. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ColorMode { + /// The entire path is one solid color, this is the default. + Solid(Color32), + + /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. + /// + /// **This cannot be serialized** + #[cfg_attr(feature = "serde", serde(skip))] + UV(Arc Color32 + Send + Sync>>), +} + +impl Default for ColorMode { + fn default() -> Self { + Self::Solid(Color32::TRANSPARENT) + } +} + +impl Debug for ColorMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), + Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), + } + } +} + +impl PartialEq for ColorMode { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Solid(l0), Self::Solid(r0)) => l0 == r0, + (Self::UV(_l0), Self::UV(_r0)) => false, + _ => false, + } + } +} diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index c21f41bd108..8a77015ab47 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -26,6 +26,7 @@ #![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] mod bezier; +pub mod color; pub mod image; mod margin; mod mesh; @@ -44,6 +45,7 @@ pub mod util; pub use self::{ bezier::{CubicBezierShape, QuadraticBezierShape}, + color::ColorMode, image::{ColorImage, FontImage, ImageData, ImageDelta}, margin::Margin, mesh::{Mesh, Mesh16, Vertex}, @@ -53,7 +55,7 @@ pub use self::{ Rounding, Shape, TextShape, }, stats::PaintStats, - stroke::{ColorMode, PathStroke, Stroke}, + stroke::{PathStroke, Stroke}, tessellator::{TessellationOptions, Tessellator}, text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 296f5d9f483..e7e34369f96 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -11,8 +11,8 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { } } Shape::LineSegment { stroke, points: _ } => match stroke.color { - stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), - stroke::ColorMode::UV(_) => {} + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + color::ColorMode::UV(_) => {} }, Shape::Path(PathShape { @@ -35,8 +35,8 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { }) => { adjust_color(fill); match stroke.color { - stroke::ColorMode::Solid(mut col) => adjust_color(&mut col), - stroke::ColorMode::UV(_) => {} + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + color::ColorMode::UV(_) => {} } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index dda1f646564..936d15d5fb7 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,45 +55,6 @@ impl std::hash::Hash for Stroke { } } -/// How paths will be colored. -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ColorMode { - /// The entire path is one solid color, this is the default. - Solid(Color32), - - /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. - /// - /// **This cannot be serialized** - #[cfg_attr(feature = "serde", serde(skip))] - UV(Arc Color32 + Send + Sync>>), -} - -impl Default for ColorMode { - fn default() -> Self { - Self::Solid(Color32::TRANSPARENT) - } -} - -impl Debug for ColorMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), - Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), - } - } -} - -impl PartialEq for ColorMode { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Solid(l0), Self::Solid(r0)) => l0 == r0, - (Self::UV(_l0), Self::UV(_r0)) => false, - _ => false, - } - } -} - /// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] /// /// The default stroke is the same as [`Stroke::NONE`]. diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0e8ac0e7d5b..fdfd625c542 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,7 +9,8 @@ use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; -use self::stroke::{ColorMode, PathStroke}; +use self::color::ColorMode; +use self::stroke::PathStroke; // ---------------------------------------------------------------------------- From 2d8ab0ce5d44c936657d61f815ca3eb4426febe4 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 09:56:43 -0600 Subject: [PATCH 11/27] add transparent helper --- crates/epaint/src/color.rs | 4 ++++ crates/epaint/src/tessellator.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs index 5da0a152b26..bddbcba2c48 100644 --- a/crates/epaint/src/color.rs +++ b/crates/epaint/src/color.rs @@ -41,3 +41,7 @@ impl PartialEq for ColorMode { } } } + +impl ColorMode { + pub const TRANSPARENT: Self = Self::Solid(Color32::TRANSPARENT); +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdfd625c542..c52d1d06372 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -878,7 +878,7 @@ fn stroke_path( ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == ColorMode::Solid(Color32::TRANSPARENT) || n < 2 { + if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 { return; } From a8e0e57dc2a4e0cc2933dd3c8bfd146beb88d2de Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 11:20:05 -0600 Subject: [PATCH 12/27] add proper stroke color demo --- Cargo.lock | 83 +++++++++++++++++ crates/egui_demo_lib/Cargo.toml | 3 + .../src/demo/dancing_trans_strings.rs | 90 +++++++++++++++++++ .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + examples/hello_world/src/main.rs | 35 +------- 6 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/dancing_trans_strings.rs diff --git a/Cargo.lock b/Cargo.lock index c6d8727188a..4837da167e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,6 +847,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorgrad" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5f405d474b9d05e0a093d3120e77e9bf26461b57a84b40aa2a221ac5617fb6" +dependencies = [ + "csscolorparser", +] + [[package]] name = "com" version = "0.6.0" @@ -1042,6 +1051,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "phf", +] + [[package]] name = "cursor-icon" version = "1.1.0" @@ -1315,12 +1333,15 @@ name = "egui_demo_lib" version = "0.27.2" dependencies = [ "chrono", + "colorgrad", "criterion", "document-features", "egui", "egui_extras", "egui_plot", "log", + "noise", + "once_cell", "serde", "unicode_names2", ] @@ -2511,6 +2532,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "noise" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc" +dependencies = [ + "num-traits", + "rand", + "rand_xorshift", +] + [[package]] name = "nom" version = "7.1.3" @@ -2761,6 +2793,48 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -3008,6 +3082,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "raw-window-handle" version = "0.5.2" diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index a08a88fe8d0..1083f5b8b47 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -50,6 +50,9 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } serde = { version = "1", optional = true, features = ["derive"] } +once_cell = "1.19" +colorgrad = "0.6" +noise = "0.9" [dev-dependencies] diff --git a/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs b/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs new file mode 100644 index 00000000000..10ca84e092b --- /dev/null +++ b/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs @@ -0,0 +1,90 @@ +use colorgrad::{Color, CustomGradient, Gradient}; +use egui::{containers::*, epaint::PathStroke, *}; +use noise::{NoiseFn, OpenSimplex}; +use once_cell::sync::Lazy; + +static GRADIENT: Lazy = Lazy::new(|| { + CustomGradient::new() + .colors(&[ + Color::from_html("#5BCEFA").unwrap(), + Color::from_html("#F5A9B8").unwrap(), + Color::from_html("#FFFFFF").unwrap(), + Color::from_html("#F5A9B8").unwrap(), + Color::from_html("#5BCEFA").unwrap(), + ]) + .build() + .unwrap_or(colorgrad::rainbow()) +}); +static NOISE: Lazy = Lazy::new(|| OpenSimplex::new(6940)); + +#[derive(Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct DancingStrings {} + +impl super::Demo for DancingStrings { + fn name(&self) -> &'static str { + "♫ Dancing Strings (Colored)" + } + + fn show(&mut self, ctx: &Context, open: &mut bool) { + use super::View as _; + Window::new(self.name()) + .open(open) + .default_size(vec2(512.0, 256.0)) + .vscroll(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for DancingStrings { + fn ui(&mut self, ui: &mut Ui) { + Frame::canvas(ui.style()).show(ui, |ui| { + ui.ctx().request_repaint(); + let time = ui.input(|i| i.time); + + let desired_size = ui.available_width() * vec2(1.0, 0.35); + let (_id, rect) = ui.allocate_space(desired_size); + + let to_screen = + emath::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] { + let mode = mode as f64; + let n = 120; + let speed = 1.5; + + let points: Vec = (0..=n) + .map(|i| { + let t = i as f64 / (n as f64); + let amp = (time * speed * mode).sin() / mode; + let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); + to_screen * pos2(t as f32, y as f32) + }) + .collect(); + + let thickness = 10.0 / mode as f32; + shapes.push(epaint::Shape::line( + points, + PathStroke::new_uv(thickness, move |_r, p| { + let time = time / 10.0; + let x = remap(p.x, rect.x_range(), 0.0..=1.0) as f64; + let y = remap(p.y, rect.y_range(), 0.0..=1.0) as f64; + + let noise = NOISE.get([x * 1.25 + time, y * 1.25 + time]); + let color = GRADIENT.at(noise).to_rgba8(); + + Color32::from_rgba_premultiplied(color[0], color[1], color[2], color[3]) + }), + )); + } + + ui.painter().extend(shapes); + }); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + } +} diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 0910df4ef56..b6647123abd 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -26,6 +26,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 8a911c9d68b..95c9fa7dfaa 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -9,6 +9,7 @@ pub mod code_editor; pub mod code_example; pub mod context_menu; pub mod dancing_strings; +pub mod dancing_trans_strings; pub mod demo_app_windows; pub mod drag_and_drop; pub mod extra_viewport; diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index a72d8b25cb6..b3fda5a5810 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -1,11 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -use std::sync::Arc; - -use eframe::{ - egui::{self, remap_clamp, vec2, Color32, Sense, Shape}, - epaint::PathStroke, -}; +use eframe::egui; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -57,34 +52,6 @@ impl eframe::App for MyApp { ui.image(egui::include_image!( "../../../crates/egui/assets/ferris.png" )); - - let (rect, _) = - ui.allocate_exact_size(vec2(100.0, 50.0), Sense::focusable_noninteractive()); - - let stroke = PathStroke { - width: 1.5, - color: egui::epaint::ColorMode::UV(Arc::new(Box::new(|r, p| { - let t = remap_clamp(p.x, r.x_range(), 0.0..=1.0); - if t < 0.5 { - Color32::RED - } else { - Color32::GREEN - } - }))), - }; - - ui.painter_at(rect).add(Shape::line_segment( - [rect.left_center(), rect.right_center()], - stroke.clone(), - )); - - ui.painter_at(rect).add(Shape::line_segment( - [ - rect.left_center() + vec2(25.0, 15.0), - rect.right_center() + vec2(0.0, 15.0), - ], - stroke.clone(), - )); }); } } From f8e6b65007f118ab2bed8cb7521f9291d5a8dc92 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 11:46:35 -0600 Subject: [PATCH 13/27] actually use the ColorMode::TRANSPARENT --- crates/epaint/src/stroke.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 936d15d5fb7..ef8c0d7f4d4 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -69,7 +69,7 @@ impl PathStroke { /// Same as [`PathStroke::default`]. pub const NONE: Self = Self { width: 0.0, - color: ColorMode::Solid(Color32::TRANSPARENT), + color: ColorMode::TRANSPARENT, }; #[inline] @@ -94,7 +94,7 @@ impl PathStroke { /// True if width is zero or color is solid and transparent #[inline] pub fn is_empty(&self) -> bool { - self.width <= 0.0 || self.color == ColorMode::Solid(Color32::TRANSPARENT) + self.width <= 0.0 || self.color == ColorMode::TRANSPARENT } } From b6c4ce2a05c30646f456e0ac8773264615702847 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 11:58:19 -0600 Subject: [PATCH 14/27] expand the bounding box to include the thickness of the path --- crates/epaint/src/tessellator.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c52d1d06372..a00b63af4a7 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -884,7 +884,9 @@ fn stroke_path( let idx = out.vertices.len() as u32; - let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()); + // expand the bounding box to include the thickness of the path + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand(stroke.width / 2.0); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, From 46cd485950154d981c981af91a6c4cbfde00c8d0 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 15:31:11 -0600 Subject: [PATCH 15/27] add a few more benchmarks --- crates/epaint/benches/benchmark.rs | 36 ++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index c27aa83f9e5..3c94499a85b 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -87,6 +87,21 @@ fn thick_line_solid(c: &mut Criterion) { }); } +fn thick_large_line_solid(c: &mut Criterion) { + c.bench_function("thick_large_solid_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + fn thin_line_solid(c: &mut Criterion) { c.bench_function("thin_solid_line", move |b| { let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; @@ -95,7 +110,22 @@ fn thin_line_solid(c: &mut Criterion) { b.iter(|| { let mut mesh = Mesh::default(); - path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thin_large_line_solid(c: &mut Criterion) { + c.bench_function("thin_large_solid_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh); black_box(mesh); }); @@ -108,6 +138,8 @@ criterion_group!( many_dashed_lines, tessellate_circles, thick_line_solid, - thin_line_solid + thick_large_line_solid, + thin_line_solid, + thin_large_line_solid ); criterion_main!(benches); From 713f566c526d8dd961316f687344687b38c1a81c Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 16 Apr 2024 18:58:10 -0600 Subject: [PATCH 16/27] add a unit test to check if all points are within the given bounding box, and make each individual tessellated point invoke the callback --- crates/epaint/src/color.rs | 1 + crates/epaint/src/stroke.rs | 2 + crates/epaint/src/tessellator.rs | 122 ++++++++++++++++++++++++++----- 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs index bddbcba2c48..b5d719c840d 100644 --- a/crates/epaint/src/color.rs +++ b/crates/epaint/src/color.rs @@ -11,6 +11,7 @@ pub enum ColorMode { Solid(Color32), /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. + /// When used with a path, the bounding box will have a margin of ``feathering + 1.0`` /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index ef8c0d7f4d4..c797996d834 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -80,6 +80,8 @@ impl PathStroke { } } + /// Create a new `PathStroke` with a UV function + /// The bounding box passed to the callback will have a margin of ``feathering + 1.0`` #[inline] pub fn new_uv( width: impl Into, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a00b63af4a7..d2e453f1b6b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -885,8 +885,9 @@ fn stroke_path( let idx = out.vertices.len() as u32; // expand the bounding box to include the thickness of the path + // this tiny bit of margin is here because a rect doesn't think it contains a point if a pos' component is on a boundary. why? i have no idea. let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) - .expand(stroke.width / 2.0); + .expand((stroke.width / 2.0) + feathering + 1.0); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, @@ -965,10 +966,15 @@ fn stroke_path( let p1 = &path[i1 as usize]; let p = p1.pos; let n = p1.normal; - let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color); - out.colored_vertex(p - n * inner_rad, color); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1006,10 +1012,15 @@ fn stroke_path( let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; - let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color); - out.colored_vertex(p - n * inner_rad, color); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 0, idx + 1, idx + 2); @@ -1021,10 +1032,15 @@ fn stroke_path( let point = &path[i1 as usize]; let p = point.pos; let n = point.normal; - let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color); - out.colored_vertex(p - n * inner_rad, color); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1045,10 +1061,15 @@ fn stroke_path( let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; - let color = get_color(color_inner, p); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color); - out.colored_vertex(p - n * inner_rad, color); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1101,15 +1122,38 @@ fn stroke_path( } } for p in path { - let color = mul_color(get_color(&stroke.color, p.pos), stroke.width / feathering); - out.colored_vertex(p.pos + radius * p.normal, color); - out.colored_vertex(p.pos - radius * p.normal, color); + out.colored_vertex( + p.pos + radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos + radius * p.normal), + stroke.width / feathering, + ), + ); + out.colored_vertex( + p.pos - radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos - radius * p.normal), + stroke.width / feathering, + ), + ); } } else { let radius = stroke.width / 2.0; for p in path { - out.colored_vertex(p.pos + radius * p.normal, get_color(&stroke.color, p.pos)); - out.colored_vertex(p.pos - radius * p.normal, get_color(&stroke.color, p.pos)); + out.colored_vertex( + p.pos + radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos + radius * p.normal), + stroke.width / feathering, + ), + ); + out.colored_vertex( + p.pos - radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos - radius * p.normal), + stroke.width / feathering, + ), + ); } } } @@ -2024,3 +2068,45 @@ fn test_tessellator() { assert_eq!(primitives.len(), 2); } + +#[test] +fn path_bounding_box() { + use crate::*; + + for i in 1..=100 { + let width = i as f32; + + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(10.0, 10.0)); + let expected_rect = rect.expand((width / 2.0) + 2.5); + + let mut mesh = Mesh::default(); + + let mut path = Path::default(); + path.add_open_points(&[ + pos2(0.0, 0.0), + pos2(2.0, 0.0), + pos2(5.0, 5.0), + pos2(0.0, 5.0), + pos2(0.0, 7.0), + pos2(10.0, 10.0), + ]); + + path.stroke( + 1.5, + PathType::Closed, + &PathStroke::new_uv(width, move |r, p| { + assert_eq!(r, expected_rect); + assert!( + r.contains(p), + "passed rect {r:?} didn't contain point {p:?}" + ); + assert!( + expected_rect.contains(p), + "expected rect {expected_rect:?} didn't contain point {p:?}" + ); + Color32::WHITE + }), + &mut mesh, + ); + } +} From 4884aadda7233466986d75892667311096f373ae Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 20 Apr 2024 17:36:25 -0600 Subject: [PATCH 17/27] have UV color modes be affected by tints --- crates/egui/src/painter.rs | 4 ++-- crates/epaint/src/shape_transform.rs | 29 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 9baeb208191..0935587cc7b 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -513,7 +513,7 @@ impl Painter { } fn tint_shape_towards(shape: &mut Shape, target: Color32) { - epaint::shape_transform::adjust_colors(shape, &|color| { + epaint::shape_transform::adjust_colors(shape, move |color| { if *color != Color32::PLACEHOLDER { *color = crate::ecolor::tint_color_towards(*color, target); } @@ -521,7 +521,7 @@ fn tint_shape_towards(shape: &mut Shape, target: Color32) { } fn multiply_opacity(shape: &mut Shape, opacity: f32) { - epaint::shape_transform::adjust_colors(shape, &|color| { + epaint::shape_transform::adjust_colors(shape, move |color| { if *color != Color32::PLACEHOLDER { *color = color.gamma_multiply(opacity); } diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index e7e34369f96..d0ab91536d1 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -1,7 +1,12 @@ +use std::sync::Arc; + use crate::*; /// Remember to handle [`Color32::PLACEHOLDER`] specially! -pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { +pub fn adjust_colors( + shape: &mut Shape, + adjust_color: impl Fn(&mut Color32) + Send + Sync + Copy + 'static, +) { #![allow(clippy::match_same_arms)] match shape { Shape::Noop => {} @@ -10,9 +15,16 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_colors(shape, adjust_color); } } - Shape::LineSegment { stroke, points: _ } => match stroke.color { + Shape::LineSegment { stroke, points: _ } => match &stroke.color { color::ColorMode::Solid(mut col) => adjust_color(&mut col), - color::ColorMode::UV(_) => {} + color::ColorMode::UV(callback) => { + let callback = callback.clone(); + stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| { + let mut col = callback(rect, pos); + adjust_color(&mut col); + col + }))); + } }, Shape::Path(PathShape { @@ -34,9 +46,16 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { stroke, }) => { adjust_color(fill); - match stroke.color { + match &stroke.color { color::ColorMode::Solid(mut col) => adjust_color(&mut col), - color::ColorMode::UV(_) => {} + color::ColorMode::UV(callback) => { + let callback = callback.clone(); + stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| { + let mut col = callback(rect, pos); + adjust_color(&mut col); + col + }))); + } } } From f71b011c914de621844b93720fc00874bf0a02b7 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 11:11:32 -0600 Subject: [PATCH 18/27] remove dependencies for dancing strings demo (doesn't compile for me for some reason) --- Cargo.lock | 83 ------------------- crates/ecolor/Cargo.toml | 1 - crates/egui_demo_lib/Cargo.toml | 5 +- .../src/demo/dancing_trans_strings.rs | 45 +++++----- 4 files changed, 26 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4837da167e0..c6d8727188a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,15 +847,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "colorgrad" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5f405d474b9d05e0a093d3120e77e9bf26461b57a84b40aa2a221ac5617fb6" -dependencies = [ - "csscolorparser", -] - [[package]] name = "com" version = "0.6.0" @@ -1051,15 +1042,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "phf", -] - [[package]] name = "cursor-icon" version = "1.1.0" @@ -1333,15 +1315,12 @@ name = "egui_demo_lib" version = "0.27.2" dependencies = [ "chrono", - "colorgrad", "criterion", "document-features", "egui", "egui_extras", "egui_plot", "log", - "noise", - "once_cell", "serde", "unicode_names2", ] @@ -2532,17 +2511,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "noise" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc" -dependencies = [ - "num-traits", - "rand", - "rand_xorshift", -] - [[package]] name = "nom" version = "7.1.3" @@ -2793,48 +2761,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -3082,15 +3008,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "raw-window-handle" version = "0.5.2" diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index 4cdbea9124b..611b9a4dec9 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -30,7 +30,6 @@ extra_debug_asserts = [] ## Always enable additional checks. extra_asserts = [] - [dependencies] #! ### Optional dependencies diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 1083f5b8b47..8c5a703b5d6 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -38,7 +38,7 @@ syntect = ["egui_extras/syntect"] [dependencies] -egui = { workspace = true, default-features = false } +egui = { workspace = true, default-features = false, features = ["color-hex"] } egui_extras = { workspace = true, features = ["default"] } egui_plot = { workspace = true, features = ["default"] } @@ -50,9 +50,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } serde = { version = "1", optional = true, features = ["derive"] } -once_cell = "1.19" -colorgrad = "0.6" -noise = "0.9" [dev-dependencies] diff --git a/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs b/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs index 10ca84e092b..605503ad7fe 100644 --- a/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs @@ -1,21 +1,12 @@ -use colorgrad::{Color, CustomGradient, Gradient}; -use egui::{containers::*, epaint::PathStroke, *}; -use noise::{NoiseFn, OpenSimplex}; -use once_cell::sync::Lazy; - -static GRADIENT: Lazy = Lazy::new(|| { - CustomGradient::new() - .colors(&[ - Color::from_html("#5BCEFA").unwrap(), - Color::from_html("#F5A9B8").unwrap(), - Color::from_html("#FFFFFF").unwrap(), - Color::from_html("#F5A9B8").unwrap(), - Color::from_html("#5BCEFA").unwrap(), - ]) - .build() - .unwrap_or(colorgrad::rainbow()) -}); -static NOISE: Lazy = Lazy::new(|| OpenSimplex::new(6940)); +use egui::{containers::*, ecolor::*, epaint::PathStroke, *}; + +static GRADIENT: [Color32; 5] = [ + hex_color!("#5BCEFA"), + hex_color!("#F5A9B8"), + Color32::WHITE, + hex_color!("#F5A9B8"), + hex_color!("#5BCEFA"), +]; #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -73,8 +64,22 @@ impl super::View for DancingStrings { let x = remap(p.x, rect.x_range(), 0.0..=1.0) as f64; let y = remap(p.y, rect.y_range(), 0.0..=1.0) as f64; - let noise = NOISE.get([x * 1.25 + time, y * 1.25 + time]); - let color = GRADIENT.at(noise).to_rgba8(); + let amp = (time * speed * mode).sin() / mode; + let sin = amp * (time * std::f64::consts::TAU / 2.0 * mode).sin(); + + let value = x * sin + y * sin; + + let color = if value < 0.2 { + GRADIENT[0] + } else if value < 0.4 { + GRADIENT[1] + } else if value < 0.6 { + GRADIENT[2] + } else if value < 0.8 { + GRADIENT[3] + } else { + GRADIENT[4] + }; Color32::from_rgba_premultiplied(color[0], color[1], color[2], color[3]) }), From 1e85d863a0747318e63c64c1a2e1c45ee10d958a Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 11:28:11 -0600 Subject: [PATCH 19/27] roll dancing strings examples into one --- .../egui_demo_lib/src/demo/dancing_strings.rs | 50 +++++++++- .../src/demo/dancing_trans_strings.rs | 95 ------------------- .../src/demo/demo_app_windows.rs | 1 - crates/egui_demo_lib/src/demo/mod.rs | 1 - 4 files changed, 47 insertions(+), 100 deletions(-) delete mode 100644 crates/egui_demo_lib/src/demo/dancing_trans_strings.rs diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 3beb323e75d..2218638c388 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,9 +1,19 @@ -use egui::{containers::*, *}; +use egui::{containers::*, epaint::PathStroke, *}; + +static GRADIENT: [Color32; 5] = [ + hex_color!("#5BCEFA"), + hex_color!("#F5A9B8"), + Color32::WHITE, + hex_color!("#F5A9B8"), + hex_color!("#5BCEFA"), +]; #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] -pub struct DancingStrings {} +pub struct DancingStrings { + colors: bool, +} impl super::Demo for DancingStrings { fn name(&self) -> &'static str { @@ -28,6 +38,10 @@ impl super::View for DancingStrings { Color32::from_black_alpha(240) }; + ui.horizontal(|ui| ui.checkbox(&mut self.colors, "Show Colors")); + + ui.separator(); + Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); let time = ui.input(|i| i.time); @@ -55,7 +69,37 @@ impl super::View for DancingStrings { .collect(); let thickness = 10.0 / mode as f32; - shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color))); + shapes.push(epaint::Shape::line( + points, + if self.colors { + PathStroke::new_uv(thickness, move |_r, p| { + let time = time / 10.0; + let x = remap(p.x, rect.x_range(), 0.0..=1.0) as f64; + let y = remap(p.y, rect.y_range(), 0.0..=1.0) as f64; + + let amp = (time * speed * mode).sin() / mode; + let sin = amp * (time * std::f64::consts::TAU / 2.0 * mode).sin(); + + let value = x * sin + y * sin; + + let color = if value < 0.2 { + GRADIENT[0] + } else if value < 0.4 { + GRADIENT[1] + } else if value < 0.6 { + GRADIENT[2] + } else if value < 0.8 { + GRADIENT[3] + } else { + GRADIENT[4] + }; + + Color32::from_rgba_premultiplied(color[0], color[1], color[2], color[3]) + }) + } else { + PathStroke::new(thickness, color) + }, + )); } ui.painter().extend(shapes); diff --git a/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs b/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs deleted file mode 100644 index 605503ad7fe..00000000000 --- a/crates/egui_demo_lib/src/demo/dancing_trans_strings.rs +++ /dev/null @@ -1,95 +0,0 @@ -use egui::{containers::*, ecolor::*, epaint::PathStroke, *}; - -static GRADIENT: [Color32; 5] = [ - hex_color!("#5BCEFA"), - hex_color!("#F5A9B8"), - Color32::WHITE, - hex_color!("#F5A9B8"), - hex_color!("#5BCEFA"), -]; - -#[derive(Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] -pub struct DancingStrings {} - -impl super::Demo for DancingStrings { - fn name(&self) -> &'static str { - "♫ Dancing Strings (Colored)" - } - - fn show(&mut self, ctx: &Context, open: &mut bool) { - use super::View as _; - Window::new(self.name()) - .open(open) - .default_size(vec2(512.0, 256.0)) - .vscroll(false) - .show(ctx, |ui| self.ui(ui)); - } -} - -impl super::View for DancingStrings { - fn ui(&mut self, ui: &mut Ui) { - Frame::canvas(ui.style()).show(ui, |ui| { - ui.ctx().request_repaint(); - let time = ui.input(|i| i.time); - - let desired_size = ui.available_width() * vec2(1.0, 0.35); - let (_id, rect) = ui.allocate_space(desired_size); - - let to_screen = - emath::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] { - let mode = mode as f64; - let n = 120; - let speed = 1.5; - - let points: Vec = (0..=n) - .map(|i| { - let t = i as f64 / (n as f64); - let amp = (time * speed * mode).sin() / mode; - let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); - to_screen * pos2(t as f32, y as f32) - }) - .collect(); - - let thickness = 10.0 / mode as f32; - shapes.push(epaint::Shape::line( - points, - PathStroke::new_uv(thickness, move |_r, p| { - let time = time / 10.0; - let x = remap(p.x, rect.x_range(), 0.0..=1.0) as f64; - let y = remap(p.y, rect.y_range(), 0.0..=1.0) as f64; - - let amp = (time * speed * mode).sin() / mode; - let sin = amp * (time * std::f64::consts::TAU / 2.0 * mode).sin(); - - let value = x * sin + y * sin; - - let color = if value < 0.2 { - GRADIENT[0] - } else if value < 0.4 { - GRADIENT[1] - } else if value < 0.6 { - GRADIENT[2] - } else if value < 0.8 { - GRADIENT[3] - } else { - GRADIENT[4] - }; - - Color32::from_rgba_premultiplied(color[0], color[1], color[2], color[3]) - }), - )); - } - - ui.painter().extend(shapes); - }); - ui.vertical_centered(|ui| { - ui.add(crate::egui_github_link_file!()); - }); - } -} diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index b6647123abd..0910df4ef56 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -26,7 +26,6 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), - Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 95c9fa7dfaa..8a911c9d68b 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -9,7 +9,6 @@ pub mod code_editor; pub mod code_example; pub mod context_menu; pub mod dancing_strings; -pub mod dancing_trans_strings; pub mod demo_app_windows; pub mod drag_and_drop; pub mod extra_viewport; From cdc65888048edaf3a872b069aa4896e019387eea Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 12:27:45 -0600 Subject: [PATCH 20/27] from_hex! can't be used in statics --- Cargo.lock | 1 + crates/egui_demo_lib/Cargo.toml | 2 ++ .../egui_demo_lib/src/demo/dancing_strings.rs | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8698db26e7a..8d858c4eacb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,6 +1321,7 @@ dependencies = [ "egui_extras", "egui_plot", "log", + "once_cell", "serde", "unicode_names2", ] diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 8c5a703b5d6..533b239a908 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -42,6 +42,8 @@ egui = { workspace = true, default-features = false, features = ["color-hex"] } egui_extras = { workspace = true, features = ["default"] } egui_plot = { workspace = true, features = ["default"] } +once_cell = "1.19" + log.workspace = true unicode_names2 = { version = "0.6.0", default-features = false } # this old version has fewer dependencies diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 2218638c388..626edfbab8d 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,12 +1,15 @@ use egui::{containers::*, epaint::PathStroke, *}; - -static GRADIENT: [Color32; 5] = [ - hex_color!("#5BCEFA"), - hex_color!("#F5A9B8"), - Color32::WHITE, - hex_color!("#F5A9B8"), - hex_color!("#5BCEFA"), -]; +use once_cell::sync::Lazy; + +static GRADIENT: Lazy<[Color32; 5]> = Lazy::new(|| { + [ + hex_color!("#5BCEFA"), + hex_color!("#F5A9B8"), + Color32::WHITE, + hex_color!("#F5A9B8"), + hex_color!("#5BCEFA"), + ] +}); #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] From 38bc7708e3a3acb0b50e6664a3c14db187524e87 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 13:15:50 -0600 Subject: [PATCH 21/27] remove margin, it seems that points can exit the rect by at most 0.55, i don't know why --- crates/epaint/src/tessellator.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index d2e453f1b6b..566d11502f0 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -885,9 +885,8 @@ fn stroke_path( let idx = out.vertices.len() as u32; // expand the bounding box to include the thickness of the path - // this tiny bit of margin is here because a rect doesn't think it contains a point if a pos' component is on a boundary. why? i have no idea. let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) - .expand((stroke.width / 2.0) + feathering + 1.0); + .expand((stroke.width / 2.0) + feathering); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, @@ -2077,7 +2076,7 @@ fn path_bounding_box() { let width = i as f32; let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(10.0, 10.0)); - let expected_rect = rect.expand((width / 2.0) + 2.5); + let expected_rect = rect.expand((width / 2.0) + 1.5); let mut mesh = Mesh::default(); @@ -2096,12 +2095,15 @@ fn path_bounding_box() { PathType::Closed, &PathStroke::new_uv(width, move |r, p| { assert_eq!(r, expected_rect); + // see https://github.com/emilk/egui/pull/4353#discussion_r1573879940 for why .contains() isn't used here. + // TL;DR rounding errors. assert!( - r.contains(p), - "passed rect {r:?} didn't contain point {p:?}" + r.distance_to_pos(p) <= 0.55, + "passed rect {r:?} didn't contain point {p:?} (distance: {})", + r.distance_to_pos(p) ); assert!( - expected_rect.contains(p), + expected_rect.distance_to_pos(p) <= 0.55, "expected rect {expected_rect:?} didn't contain point {p:?}" ); Color32::WHITE From b1f7520f34572486f1a56160d3ef83ea3450d06d Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 15:21:46 -0600 Subject: [PATCH 22/27] remove unesecarry box for closure --- crates/epaint/src/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs index b5d719c840d..289783c70fb 100644 --- a/crates/epaint/src/color.rs +++ b/crates/epaint/src/color.rs @@ -15,7 +15,7 @@ pub enum ColorMode { /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] - UV(Arc Color32 + Send + Sync>>), + UV(Arc Color32 + Send + Sync>), } impl Default for ColorMode { From 2283268a307aa6d1add9ce4a96c858518e94ea7d Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 15:59:10 -0600 Subject: [PATCH 23/27] add benchmarks for uv mode lines --- crates/epaint/benches/benchmark.rs | 94 +++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 3c94499a85b..6323137fa50 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -132,6 +132,94 @@ fn thin_large_line_solid(c: &mut Criterion) { }); } +fn thick_line_uv(c: &mut Criterion) { + c.bench_function("thick_uv_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thick_large_line_uv(c: &mut Criterion) { + c.bench_function("thick_large_uv_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thin_line_uv(c: &mut Criterion) { + c.bench_function("thin_uv_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thin_large_line_uv(c: &mut Criterion) { + c.bench_function("thin_large_uv_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + criterion_group!( benches, single_dashed_lines, @@ -140,6 +228,10 @@ criterion_group!( thick_line_solid, thick_large_line_solid, thin_line_solid, - thin_large_line_solid + thin_large_line_solid, + thick_line_uv, + thick_large_line_uv, + thin_line_uv, + thin_large_line_uv ); criterion_main!(benches); From 5f3a7128072a41c38f7b3cb0717d08a8ca793856 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 20:06:25 -0600 Subject: [PATCH 24/27] why are you still here, mx. box? --- crates/epaint/src/stroke.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index c797996d834..cf95d1ed7a9 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -89,7 +89,7 @@ impl PathStroke { ) -> Self { Self { width: width.into(), - color: ColorMode::UV(Arc::new(Box::new(callback))), + color: ColorMode::UV(Arc::new(callback)), } } From 1a95c4800e60328a43335a8fe91e604e6115dafc Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Apr 2024 20:14:21 -0600 Subject: [PATCH 25/27] fix docs --- crates/epaint/src/color.rs | 2 +- crates/epaint/src/stroke.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs index 289783c70fb..54106c10d3f 100644 --- a/crates/epaint/src/color.rs +++ b/crates/epaint/src/color.rs @@ -11,7 +11,7 @@ pub enum ColorMode { Solid(Color32), /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. - /// When used with a path, the bounding box will have a margin of ``feathering + 1.0`` + /// When used with a path, the bounding box will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`) /// /// **This cannot be serialized** #[cfg_attr(feature = "serde", serde(skip))] diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index cf95d1ed7a9..54d7fe20e56 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -81,7 +81,8 @@ impl PathStroke { } /// Create a new `PathStroke` with a UV function - /// The bounding box passed to the callback will have a margin of ``feathering + 1.0`` + /// + /// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`) #[inline] pub fn new_uv( width: impl Into, From 52a645935a32dd9f02140053709d3577293ce158 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 22 Apr 2024 08:14:44 -0600 Subject: [PATCH 26/27] remove mul_color --- crates/epaint/src/tessellator.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 566d11502f0..c7dc31b75ab 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1141,17 +1141,11 @@ fn stroke_path( for p in path { out.colored_vertex( p.pos + radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos + radius * p.normal), - stroke.width / feathering, - ), + get_color(&stroke.color, p.pos + radius * p.normal), ); out.colored_vertex( p.pos - radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos - radius * p.normal), - stroke.width / feathering, - ), + get_color(&stroke.color, p.pos - radius * p.normal), ); } } From a92ed912db2cb58ecbe1f3431c497c77c991d49c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 22 Apr 2024 18:23:36 +0200 Subject: [PATCH 27/27] Simplify the dancing strings example --- Cargo.lock | 1 - crates/egui_demo_lib/Cargo.toml | 2 - .../egui_demo_lib/src/demo/dancing_strings.rs | 49 +++++-------------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d858c4eacb..8698db26e7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,7 +1321,6 @@ dependencies = [ "egui_extras", "egui_plot", "log", - "once_cell", "serde", "unicode_names2", ] diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 533b239a908..8c5a703b5d6 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -42,8 +42,6 @@ egui = { workspace = true, default-features = false, features = ["color-hex"] } egui_extras = { workspace = true, features = ["default"] } egui_plot = { workspace = true, features = ["default"] } -once_cell = "1.19" - log.workspace = true unicode_names2 = { version = "0.6.0", default-features = false } # this old version has fewer dependencies diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 626edfbab8d..a2b560ee723 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,15 +1,4 @@ use egui::{containers::*, epaint::PathStroke, *}; -use once_cell::sync::Lazy; - -static GRADIENT: Lazy<[Color32; 5]> = Lazy::new(|| { - [ - hex_color!("#5BCEFA"), - hex_color!("#F5A9B8"), - Color32::WHITE, - hex_color!("#F5A9B8"), - hex_color!("#5BCEFA"), - ] -}); #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -41,9 +30,8 @@ impl super::View for DancingStrings { Color32::from_black_alpha(240) }; - ui.horizontal(|ui| ui.checkbox(&mut self.colors, "Show Colors")); - - ui.separator(); + ui.checkbox(&mut self.colors, "Colored") + .on_hover_text("Demonstrates how a path can have varying color across its length."); Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); @@ -75,29 +63,16 @@ impl super::View for DancingStrings { shapes.push(epaint::Shape::line( points, if self.colors { - PathStroke::new_uv(thickness, move |_r, p| { - let time = time / 10.0; - let x = remap(p.x, rect.x_range(), 0.0..=1.0) as f64; - let y = remap(p.y, rect.y_range(), 0.0..=1.0) as f64; - - let amp = (time * speed * mode).sin() / mode; - let sin = amp * (time * std::f64::consts::TAU / 2.0 * mode).sin(); - - let value = x * sin + y * sin; - - let color = if value < 0.2 { - GRADIENT[0] - } else if value < 0.4 { - GRADIENT[1] - } else if value < 0.6 { - GRADIENT[2] - } else if value < 0.8 { - GRADIENT[3] - } else { - GRADIENT[4] - }; - - Color32::from_rgba_premultiplied(color[0], color[1], color[2], color[3]) + PathStroke::new_uv(thickness, move |rect, p| { + let t = remap(p.x, rect.x_range(), -1.0..=1.0).abs(); + let center_color = hex_color!("#5BCEFA"); + let outer_color = hex_color!("#F5A9B8"); + + Color32::from_rgb( + lerp(center_color.r() as f32..=outer_color.r() as f32, t) as u8, + lerp(center_color.g() as f32..=outer_color.g() as f32, t) as u8, + lerp(center_color.b() as f32..=outer_color.b() as f32, t) as u8, + ) }) } else { PathStroke::new(thickness, color)