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/src/painter.rs b/crates/egui/src/painter.rs index 788319dc936..0935587cc7b 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( @@ -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/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index a08a88fe8d0..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"] } diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 3beb323e75d..a2b560ee723 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,9 +1,11 @@ -use egui::{containers::*, *}; +use egui::{containers::*, epaint::PathStroke, *}; #[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 +30,9 @@ impl super::View for DancingStrings { Color32::from_black_alpha(240) }; + 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(); let time = ui.input(|i| i.time); @@ -55,7 +60,24 @@ 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 |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) + }, + )); } ui.painter().extend(shapes); diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 709adbfae9c..6323137fa50 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,166 @@ 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 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)]; + 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); + }); + }); +} + +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); + }); + }); +} + +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, many_dashed_lines, - tessellate_circles + tessellate_circles, + thick_line_solid, + thick_large_line_solid, + thin_line_solid, + thin_large_line_solid, + thick_line_uv, + thick_large_line_uv, + thin_line_uv, + thin_large_line_uv ); criterion_main!(benches); 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/color.rs b/crates/epaint/src/color.rs new file mode 100644 index 00000000000..54106c10d3f --- /dev/null +++ b/crates/epaint/src/color.rs @@ -0,0 +1,48 @@ +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. + /// 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))] + 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, + } + } +} + +impl ColorMode { + pub const TRANSPARENT: Self = Self::Solid(Color32::TRANSPARENT); +} diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 18298484679..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::Stroke, + stroke::{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..336c9087316 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). @@ -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..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,8 +15,48 @@ 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 { + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + 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 { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::QuadraticBezier(QuadraticBezierShape { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::CubicBezier(CubicBezierShape { + points: _, + closed: _, + fill, + stroke, + }) => { + adjust_color(fill); + match &stroke.color { + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + 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::Circle(CircleShape { @@ -26,12 +71,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 +79,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..54d7fe20e56 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,68 @@ impl std::hash::Hash for Stroke { color.hash(state); } } + +/// 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 { + pub width: f32, + pub color: ColorMode, +} + +impl PathStroke { + /// Same as [`PathStroke::default`]. + pub const NONE: Self = Self { + width: 0.0, + color: ColorMode::TRANSPARENT, + }; + + #[inline] + pub fn new(width: impl Into, color: impl Into) -> Self { + Self { + width: width.into(), + color: ColorMode::Solid(color.into()), + } + } + + /// Create a new `PathStroke` with a UV function + /// + /// 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, + callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static, + ) -> Self { + Self { + width: width.into(), + color: ColorMode::UV(Arc::new(callback)), + } + } + + /// 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::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..c7dc31b75ab 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,6 +9,9 @@ use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; +use self::color::ColorMode; +use self::stroke::PathStroke; + // ---------------------------------------------------------------------------- #[allow(clippy::approx_constant)] @@ -471,16 +474,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 +873,28 @@ 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::TRANSPARENT || n < 2 { return; } let idx = out.vertices.len() as u32; + // 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) + feathering); + + let get_color = |col: &ColorMode, pos: Pos2| match col { + ColorMode::Solid(col) => *col, + ColorMode::UV(fun) => fun(bbox, pos), + }; + 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 +907,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 +924,10 @@ 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, + mul_color(get_color(color_inner, p), stroke.width / feathering), + ); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { @@ -943,8 +966,14 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; 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, + 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); @@ -983,8 +1012,14 @@ fn stroke_path( let n = end.normal; let back_extrude = n.rot90() * feathering; 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, + 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); @@ -997,8 +1032,14 @@ fn stroke_path( let p = point.pos; let n = point.normal; 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, + 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); @@ -1020,8 +1061,14 @@ fn stroke_path( let n = end.normal; let back_extrude = -n.rot90() * feathering; 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, + 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); @@ -1067,19 +1114,39 @@ 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 { - 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, 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 + radius * p.normal), + ); + out.colored_vertex( + p.pos - radius * p.normal, + get_color(&stroke.color, p.pos - radius * p.normal), + ); } } } @@ -1275,9 +1342,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 +1404,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 +1471,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 +1497,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 +1519,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 +1566,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 +1661,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 +1780,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 +1795,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 +1813,7 @@ impl Tessellator { &points, quadratic_shape.fill, quadratic_shape.closed, - quadratic_shape.stroke, + &quadratic_shape.stroke, out, ); } @@ -1746,7 +1822,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 +1839,7 @@ impl Tessellator { &points, cubic_shape.fill, cubic_shape.closed, - cubic_shape.stroke, + &cubic_shape.stroke, out, ); } @@ -1774,7 +1850,7 @@ impl Tessellator { points: &[Pos2], fill: Color32, closed: bool, - stroke: Stroke, + stroke: &PathStroke, out: &mut Mesh, ) { if points.len() < 2 { @@ -1985,3 +2061,48 @@ 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) + 1.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); + // see https://github.com/emilk/egui/pull/4353#discussion_r1573879940 for why .contains() isn't used here. + // TL;DR rounding errors. + assert!( + r.distance_to_pos(p) <= 0.55, + "passed rect {r:?} didn't contain point {p:?} (distance: {})", + r.distance_to_pos(p) + ); + assert!( + expected_rect.distance_to_pos(p) <= 0.55, + "expected rect {expected_rect:?} didn't contain point {p:?}" + ); + Color32::WHITE + }), + &mut mesh, + ); + } +} diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index c9956aac197..276f3f9e2a3 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