Added ability to define colors at UV coordinates along a path (#4353)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to add commits to your PR.
* Remember to run `cargo fmt` and `cargo cranky`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

I had to make a couple types not Copy because closures, but it should'nt
be a massive deal.

I tried my best to make the API change as non breaking as possible.
Anywhere a PathStroke is used, you can just use a normal Stroke instead.
As mentioned above, the bezier paths couldn't be copy anymore, but IMO
that's a minor caveat.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Joe Sorensen 2024-04-22 10:35:09 -06:00 committed by GitHub
parent ff8cfc2aa0
commit 2ce82cce21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 550 additions and 104 deletions

View File

@ -30,7 +30,6 @@ extra_debug_asserts = []
## Always enable additional checks.
extra_asserts = []
[dependencies]
#! ### Optional dependencies

View File

@ -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<Stroke>) -> ShapeIdx {
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PathStroke>) -> 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<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke))
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke.into()))
}
/// Paints a vertical line.
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::vline(x, y, stroke))
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> 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);
}

View File

@ -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"] }

View File

@ -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);

View File

@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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);

View File

@ -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>,
stroke: impl Into<PathStroke>,
) -> 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<CubicBezierShape> 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>,
stroke: impl Into<PathStroke>,
) -> 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(),
}
}

View File

@ -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<dyn Fn(Rect, Pos2) -> 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(&"<closure>").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);
}

View File

@ -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,

View File

@ -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<Stroke>) -> Self {
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<PathStroke>) -> Self {
Self::LineSegment {
points,
stroke: stroke.into(),
@ -96,7 +100,7 @@ impl Shape {
}
/// A horizontal line.
pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> Self {
pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> 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<Rangef>, stroke: impl Into<Stroke>) -> Self {
pub fn vline(x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> 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<Pos2>, stroke: impl Into<Stroke>) -> Self {
pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::line(points, stroke))
}
/// A line that closes back to the start point again.
#[inline]
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::closed_line(points, stroke))
}
@ -224,7 +228,7 @@ impl Shape {
pub fn convex_polygon(
points: Vec<Pos2>,
fill: impl Into<Color32>,
stroke: impl Into<Stroke>,
stroke: impl Into<PathStroke>,
) -> 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<Pos2>, stroke: impl Into<Stroke>) -> Self {
pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> 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<Pos2>, stroke: impl Into<Stroke>) -> Self {
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self {
points,
closed: true,
@ -623,7 +627,7 @@ impl PathShape {
pub fn convex_polygon(
points: Vec<Pos2>,
fill: impl Into<Color32>,
stroke: impl Into<Stroke>,
stroke: impl Into<PathStroke>,
) -> Self {
Self {
points,

View File

@ -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);

View File

@ -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<f32>, color: impl Into<Color32>) -> 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<f32>,
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<Color> From<(f32, Color)> for PathStroke
where
Color: Into<Color32>,
{
#[inline(always)]
fn from((width, color): (f32, Color)) -> Self {
Self::new(width, color)
}
}
impl From<Stroke> for PathStroke {
fn from(value: Stroke) -> Self {
Self {
width: value.width,
color: ColorMode::Solid(value.color),
}
}
}

View File

@ -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::<Vec<Pos2>>())
.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<PathStroke>,
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,
);
}
}

View File

@ -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