Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RectShape::blur_width to implement shadows #4267

Merged
merged 1 commit into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions crates/egui/src/containers/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,8 @@ impl Frame {
if shadow == Default::default() {
frame_shape
} else {
let shadow = shadow.tessellate(outer_rect, rounding);
let shadow = Shape::Mesh(shadow);
Shape::Vec(vec![shadow, frame_shape])
let shadow = shadow.as_shape(outer_rect, rounding);
Shape::Vec(vec![Shape::from(shadow), frame_shape])
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/widgets/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ pub fn paint_texture_at(
rounding: options.rounding,
fill: options.tint,
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: texture.id,
uv: options.uv,
});
Expand Down
10 changes: 2 additions & 8 deletions crates/egui/src/widgets/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -739,14 +739,8 @@ impl<'a> Slider<'a> {
};
let v = v + Vec2::splat(visuals.expansion);
let rect = Rect::from_center_size(center, 2.0 * v);
ui.painter().add(epaint::RectShape {
fill: visuals.bg_fill,
stroke: visuals.fg_stroke,
rect,
rounding: visuals.rounding,
fill_texture_id: Default::default(),
uv: Rect::ZERO,
});
ui.painter()
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.fg_stroke);
}
}
}
Expand Down
42 changes: 4 additions & 38 deletions crates/epaint/src/shadow.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use emath::NumExt as _;

use super::*;

/// The color and fuzziness of a fuzzy shape.
Expand Down Expand Up @@ -37,11 +35,10 @@ impl Shadow {
color: Color32::TRANSPARENT,
};

pub fn tessellate(&self, rect: Rect, rounding: impl Into<Rounding>) -> Mesh {
/// The argument is the rectangle of the shadow caster.
pub fn as_shape(&self, rect: Rect, rounding: impl Into<Rounding>) -> RectShape {
// tessellator.clip_rect = clip_rect; // TODO(emilk): culling

use crate::tessellator::*;

let Self {
offset,
blur,
Expand All @@ -50,40 +47,9 @@ impl Shadow {
} = *self;

let rect = rect.translate(offset).expand(spread);
let rounding = rounding.into() + Rounding::same(spread.abs());

// We simulate a blurry shadow by tessellating a solid rectangle using a very large feathering.
// Feathering is usually used to make the edges of a shape softer for anti-aliasing.
// The tessellator can't handle blurring/feathering larger than the smallest side of the rect.
// Thats because the tessellator approximate very thin rectangles as line segments,
// and these line segments don't have rounded corners.
// When the feathering is small (the size of a pixel), this is usually fine,
// but here we have a huge feathering to simulate blur,
// so we need to avoid this optimization in the tessellator,
// which is also why we add this rather big epsilon:
let eps = 0.1;
let blur = blur.at_most(rect.size().min_elem() - eps).at_least(0.0);

// TODO(emilk): if blur <= 0, return a simple `Shape::Rect` instead of using the tessellator

let rounding_expansion = spread.abs() + 0.5 * blur;
let rounding = rounding.into() + Rounding::same(rounding_expansion);

let rect = RectShape::filled(rect, rounding, color);
let pixels_per_point = 1.0; // doesn't matter here
let font_tex_size = [1; 2]; // unused since we are not tessellating text.
let mut tessellator = Tessellator::new(
pixels_per_point,
TessellationOptions {
feathering: true,
feathering_size_in_pixels: blur * pixels_per_point,
..Default::default()
},
font_tex_size,
vec![],
);
let mut mesh = Mesh::default();
tessellator.tessellate_rect(&rect, &mut mesh);
mesh
RectShape::filled(rect, rounding, color).with_blur_width(blur)
}

/// How much larger than the parent rect are we in each direction?
Expand Down
26 changes: 25 additions & 1 deletion crates/epaint/src/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,14 @@ pub struct RectShape {
/// The thickness and color of the outline.
pub stroke: Stroke,

/// If larger than zero, the edges of the rectangle
/// (for both fill and stroke) will be blurred.
///
/// This can be used to produce shadows and glow effects.
///
/// The blur is currently implemented using a simple linear blur in sRGBA gamma space.
pub blur_width: f32,

/// If the rect should be filled with a texture, which one?
///
/// The texture is multiplied with [`Self::fill`].
Expand Down Expand Up @@ -695,6 +703,7 @@ impl RectShape {
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
Expand All @@ -711,6 +720,7 @@ impl RectShape {
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
Expand All @@ -723,18 +733,32 @@ impl RectShape {
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
}

/// If larger than zero, the edges of the rectangle
/// (for both fill and stroke) will be blurred.
///
/// This can be used to produce shadows and glow effects.
///
/// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space.
#[inline]
pub fn with_blur_width(mut self, blur_width: f32) -> Self {
self.blur_width = blur_width;
self
}

/// The visual bounding rectangle (includes stroke width)
#[inline]
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
self.rect.expand(self.stroke.width / 2.0)
self.rect
.expand((self.stroke.width + self.blur_width) / 2.0)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/epaint/src/shape_transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
rounding: _,
fill,
stroke,
blur_width: _,
fill_texture_id: _,
uv: _,
})
Expand Down
28 changes: 27 additions & 1 deletion crates/epaint/src/tessellator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1503,9 +1503,10 @@ impl Tessellator {
pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) {
let RectShape {
mut rect,
rounding,
mut rounding,
fill,
stroke,
mut blur_width,
fill_texture_id,
uv,
} = *rect;
Expand All @@ -1524,6 +1525,29 @@ impl Tessellator {
rect.min = rect.min.at_least(pos2(-1e7, -1e7));
rect.max = rect.max.at_most(pos2(1e7, 1e7));

let old_feathering = self.feathering;

if old_feathering < blur_width {
// We accomplish the blur by using a larger-than-normal feathering.
// Feathering is usually used to make the edges of a shape softer for anti-aliasing.

// The tessellator can't handle blurring/feathering larger than the smallest side of the rect.
// Thats because the tessellator approximate very thin rectangles as line segments,
// and these line segments don't have rounded corners.
// When the feathering is small (the size of a pixel), this is usually fine,
// but here we have a huge feathering to simulate blur,
// so we need to avoid this optimization in the tessellator,
// which is also why we add this rather big epsilon:
let eps = 0.1;
blur_width = blur_width
.at_most(rect.size().min_elem() - eps)
.at_least(0.0);

rounding += Rounding::same(0.5 * blur_width);

self.feathering = self.feathering.max(blur_width);
}

if rect.width() < self.feathering {
// Very thin - approximate by a vertical line-segment:
let line = [rect.center_top(), rect.center_bottom()];
Expand Down Expand Up @@ -1566,6 +1590,8 @@ impl Tessellator {

path.stroke_closed(self.feathering, stroke, out);
}

self.feathering = old_feathering; // restore
}

/// Tessellate a single [`TextShape`] into a [`Mesh`].
Expand Down
Loading