1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 21:30:03 -04:00
Files
egui/crates/egui_demo_lib/src/rendering_test.rs
Juan Campa f2815b423e Fix blurry lines (#4943)
<!--
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 test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* 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!
-->


* Closes <https://github.com/emilk/egui/issues/4776>
* [x] I have followed the instructions in the PR template



I've been meaning to look into this for a while but finally bit the
bullet this week. Contrary to what I initially thought, the problem of
blurry lines is unrelated to feathering because it also happens with
feathering disabled.

The root cause is that lines tend to land on pixel boundaries, and
because of that, frequently used strokes (e.g. 1pt), end up partially
covering pixels. This is especially noticeable on 1ppp displays.

There were a couple of things to fix, namely: individual lines like
separators and indents but also shape strokes (e.g. Frame).

Lines were easy, I just made sure we round them to the nearest pixel
_center_, instead of the nearest pixel boundary.

Strokes were a little more complicated. To illustrate why, here’s an
example: if we're rendering a 5x5 rect (black fill, red stroke), we
would expect to see something like this:

![Screenshot 2024-08-11 at 15 01
41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6)

The fill and the stroke to cover entire pixels. Instead, egui was
painting the stroke partially inside and partially outside, centered
around the shape’s path (blue line):

![Screenshot 2024-08-11 at 15 00
57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b)

Both methods are valid for different use-cases but the first one is what
we’d typically want for UIs to feel crisp and pixel perfect. It's also
how CSS borders work (related to #4019 and #3284).

Luckily, we can use the normal computed for each `PathPoint` to adjust
the location of the stroke to be outside, inside, or in the middle.
These also are the 3 types of strokes available in tools like Photoshop.

This PR introduces an enum `StrokeKind` which determines if a
`PathStroke` should be tessellated outside, inside, or _on_ the path
itself. Where "outside" is defined by the directions normals point to.

Tessellator will now use `StrokeKind::Outside` for closed shapes like
rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's
no meaningful "outside" concept for open paths. This PR doesn't expose
`StrokeKind` to user-land, but we can implement that later so that users
can render shapes and decide where to place the stroke.

### Strokes test
(blue lines represent the size of the rect being rendered)

`Stroke::Middle` (current behavior, 1px and 3px are blurry)
![Screenshot 2024-08-09 at 23 55
48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e)


`Stroke::Outside` (proposed default behavior for closed paths)
![Screenshot 2024-08-09 at 23 51
55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85)



`Stroke::Inside` (for completeness but unused at the moment)
![Screenshot 2024-08-09 at 23 54
49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a)



### Demo App
The best way to review this PR is to run the demo on a 1ppp display,
especially to test hover effects. Everything should look crisper. Also
run it in a higher dpi screen to test that nothing broke 🙏.

Before:

![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810)


After (notice the sharper lines):

![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702)
2024-08-30 09:57:32 +02:00

683 lines
23 KiB
Rust

use std::collections::HashMap;
use egui::{
epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2, Color32, FontId, Image,
Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke, TextureHandle,
TextureOptions, Ui, Vec2,
};
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
const BLACK: Color32 = Color32::BLACK;
const GREEN: Color32 = Color32::GREEN;
const RED: Color32 = Color32::RED;
const TRANSPARENT: Color32 = Color32::TRANSPARENT;
const WHITE: Color32 = Color32::WHITE;
/// A test for sanity-checking and diagnosing egui rendering backends.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorTest {
#[cfg_attr(feature = "serde", serde(skip))]
tex_mngr: TextureManager,
vertex_gradients: bool,
texture_gradients: bool,
}
impl Default for ColorTest {
fn default() -> Self {
Self {
tex_mngr: Default::default(),
vertex_gradients: true,
texture_gradients: true,
}
}
}
impl ColorTest {
pub fn ui(&mut self, ui: &mut Ui) {
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
ui.horizontal_wrapped(|ui|{
ui.label("This is made to test that the egui rendering backend is set up correctly.");
ui.add(egui::Label::new("").sense(egui::Sense::click()))
.on_hover_text("The texture sampling should be sRGB-aware, and every other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha");
});
ui.separator();
pixel_test(ui);
ui.separator();
ui.collapsing("Color test", |ui| {
self.color_test(ui);
});
ui.separator();
ui.heading("Text rendering");
text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray
text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text
// Matches Mac Font book (useful for testing):
text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255));
text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30));
ui.separator();
blending_and_feathering_test(ui);
}
fn color_test(&mut self, ui: &mut Ui) {
ui.label("If the rendering is done right, all groups of gradients will look uniform.");
ui.horizontal(|ui| {
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
});
ui.heading("sRGB color test");
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
});
ui.separator();
ui.label("Test that vertex color times texture color is done in gamma space:");
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
let tex_color = Color32::from_rgb(64, 128, 255);
let vertex_color = Color32::from_rgb(128, 196, 196);
let ground_truth = mul_color_gamma(tex_color, vertex_color);
ui.horizontal(|ui| {
let color_size = ui.spacing().interact_size;
ui.label("texture");
show_color(ui, tex_color, color_size);
ui.label(" * ");
show_color(ui, vertex_color, color_size);
ui.label(" vertex color =");
});
{
let g = Gradient::one_color(ground_truth);
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
}
ui.horizontal(|ui| {
let g = Gradient::one_color(tex_color);
let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(
Image::from_texture((tex.id(), GRADIENT_SIZE))
.tint(vertex_color)
.uv(uv),
)
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
});
ui.separator();
// TODO(emilk): test color multiplication (image tint),
// to make sure vertex and texture color multiplication is done in linear space.
ui.label("Gamma interpolation:");
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, RED, (TRANSPARENT, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, BLACK, (BLACK, WHITE), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE), Interpolation::Gamma);
ui.separator();
ui.label("Additive blending: add more and more blue to the red background:");
self.show_gradients(
ui,
RED,
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
Interpolation::Gamma,
);
ui.separator();
ui.label("Linear interpolation (texture sampling):");
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
}
fn show_gradients(
&mut self,
ui: &mut Ui,
bg_fill: Color32,
(left, right): (Color32, Color32),
interpolation: Interpolation,
) {
let is_opaque = left.is_opaque() && right.is_opaque();
ui.horizontal(|ui| {
let color_size = ui.spacing().interact_size;
if !is_opaque {
ui.label("Background:");
show_color(ui, bg_fill, color_size);
}
ui.label("gradient");
show_color(ui, left, color_size);
ui.label("-");
show_color(ui, right, color_size);
});
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
if is_opaque {
let g = Gradient::ground_truth_gradient(left, right, interpolation);
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
} else {
let g = Gradient::ground_truth_gradient(left, right, interpolation)
.with_bg_fill(bg_fill);
self.vertex_gradient(
ui,
"Ground Truth (CPU gradient, CPU blending) - vertices",
bg_fill,
&g,
);
self.tex_gradient(
ui,
"Ground Truth (CPU gradient, CPU blending) - texture",
bg_fill,
&g,
);
let g = Gradient::ground_truth_gradient(left, right, interpolation);
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
}
let g = Gradient::endpoints(left, right);
match interpolation {
Interpolation::Linear => {
// texture sampler is sRGBA aware, and should therefore be linear
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
}
Interpolation::Gamma => {
// vertex shader uses gamma
self.vertex_gradient(
ui,
"Triangle mesh of width 2 (test vertex decode and interpolation)",
bg_fill,
&g,
);
}
}
});
}
fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
if !self.texture_gradients {
return;
}
ui.horizontal(|ui| {
let tex = self.tex_mngr.get(ui.ctx(), gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(
Image::from_texture((tex.id(), GRADIENT_SIZE))
.bg_fill(bg_fill)
.uv(uv),
)
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.label(label);
});
}
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
if !self.vertex_gradients {
return;
}
ui.horizontal(|ui| {
vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!(
"A triangle mesh that is {} vertices wide",
gradient.0.len()
));
ui.label(label);
});
}
}
fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response {
let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover());
if bg_fill != Default::default() {
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, bg_fill);
ui.painter().add(Shape::mesh(mesh));
}
{
let n = gradient.0.len();
assert!(n >= 2);
let mut mesh = Mesh::default();
for (i, &color) in gradient.0.iter().enumerate() {
let t = i as f32 / (n as f32 - 1.0);
let x = lerp(rect.x_range(), t);
mesh.colored_vertex(pos2(x, rect.top()), color);
mesh.colored_vertex(pos2(x, rect.bottom()), color);
if i < n - 1 {
let i = i as u32;
mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
}
}
ui.painter().add(Shape::mesh(mesh));
}
response
}
#[derive(Clone, Copy)]
enum Interpolation {
Linear,
Gamma,
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct Gradient(pub Vec<Color32>);
impl Gradient {
pub fn one_color(srgba: Color32) -> Self {
Self(vec![srgba, srgba])
}
pub fn endpoints(left: Color32, right: Color32) -> Self {
Self(vec![left, right])
}
pub fn ground_truth_gradient(
left: Color32,
right: Color32,
interpolation: Interpolation,
) -> Self {
match interpolation {
Interpolation::Linear => Self::ground_truth_linear_gradient(left, right),
Interpolation::Gamma => Self::ground_truth_gamma_gradient(left, right),
}
}
pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self {
let left = Rgba::from(left);
let right = Rgba::from(right);
let n = 255;
Self(
(0..=n)
.map(|i| {
let t = i as f32 / n as f32;
Color32::from(lerp(left..=right, t))
})
.collect(),
)
}
pub fn ground_truth_gamma_gradient(left: Color32, right: Color32) -> Self {
let n = 255;
Self(
(0..=n)
.map(|i| {
let t = i as f32 / n as f32;
left.lerp_to_gamma(right, t)
})
.collect(),
)
}
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
/// in gamma-space.
pub fn with_bg_fill(self, bg: Color32) -> Self {
Self(
self.0
.into_iter()
.map(|fg| {
let a = fg.a() as f32 / 255.0;
Color32::from_rgba_premultiplied(
(bg[0] as f32 * (1.0 - a) + fg[0] as f32).round() as u8,
(bg[1] as f32 * (1.0 - a) + fg[1] as f32).round() as u8,
(bg[2] as f32 * (1.0 - a) + fg[2] as f32).round() as u8,
(bg[3] as f32 * (1.0 - a) + fg[3] as f32).round() as u8,
)
})
.collect(),
)
}
pub fn to_pixel_row(&self) -> Vec<Color32> {
self.0.clone()
}
}
#[derive(Default)]
struct TextureManager(HashMap<Gradient, TextureHandle>);
impl TextureManager {
fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
self.0.entry(gradient.clone()).or_insert_with(|| {
let pixels = gradient.to_pixel_row();
let width = pixels.len();
let height = 1;
ctx.load_texture(
"color_test_gradient",
epaint::ColorImage {
size: [width, height],
pixels,
},
TextureOptions::LINEAR,
)
})
}
}
/// A visual test that the rendering is correctly aligned on the physical pixel grid.
///
/// Requires eyes and a magnifying glass to verify.
pub fn pixel_test(ui: &mut Ui) {
ui.heading("Pixel alignment test");
ui.label("If anything is blurry, then everything will be blurry, including text.");
ui.label("You might need a magnifying glass to check this test.");
if cfg!(target_arch = "wasm32") {
ui.label("Make sure these test pass even when you zoom in/out and resize the browser.");
}
ui.add_space(4.0);
pixel_test_lines(ui);
ui.add_space(4.0);
pixel_test_squares(ui);
ui.add_space(4.0);
pixel_test_strokes(ui);
}
fn pixel_test_strokes(ui: &mut Ui) {
ui.label("The strokes should align to the physical pixel grid.");
let color = if ui.style().visuals.dark_mode {
egui::Color32::WHITE
} else {
egui::Color32::BLACK
};
let pixels_per_point = ui.ctx().pixels_per_point();
for thickness_pixels in 1..=3 {
let thickness_pixels = thickness_pixels as f32;
let thickness_points = thickness_pixels / pixels_per_point;
let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32;
let size_pixels = vec2(
ui.available_width(),
num_squares as f32 + thickness_pixels * 2.0,
);
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
let mut cursor_pixel = Pos2::new(
response.rect.min.x * pixels_per_point + thickness_pixels,
response.rect.min.y * pixels_per_point + thickness_pixels,
)
.ceil();
let stroke = Stroke::new(thickness_points, color);
for size in 1..=num_squares {
let rect_points = Rect::from_min_size(
Pos2::new(cursor_pixel.x, cursor_pixel.y),
Vec2::splat(size as f32),
);
painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke);
cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0;
}
}
}
fn pixel_test_squares(ui: &mut Ui) {
ui.label("The first square should be exactly one physical pixel big.");
ui.label("They should be exactly one physical pixel apart.");
ui.label("Each subsequent square should be one physical pixel larger than the previous.");
ui.label("They should be perfectly aligned to the physical pixel grid.");
let color = if ui.style().visuals.dark_mode {
egui::Color32::WHITE
} else {
egui::Color32::BLACK
};
let pixels_per_point = ui.ctx().pixels_per_point();
let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32;
let size_pixels = vec2(
((num_squares + 1) * (num_squares + 2) / 2) as f32,
num_squares as f32,
);
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
let mut cursor_pixel = Pos2::new(
response.rect.min.x * pixels_per_point,
response.rect.min.y * pixels_per_point,
)
.ceil();
for size in 1..=num_squares {
let rect_points = Rect::from_min_size(
Pos2::new(cursor_pixel.x, cursor_pixel.y),
Vec2::splat(size as f32),
);
painter.rect_filled(rect_points / pixels_per_point, 0.0, color);
cursor_pixel.x += (1 + size) as f32;
}
}
fn pixel_test_lines(ui: &mut Ui) {
let pixels_per_point = ui.ctx().pixels_per_point();
let n = (96.0 * pixels_per_point) as usize;
ui.label("The lines should be exactly one physical pixel wide, one physical pixel apart.");
ui.label("They should be perfectly white and black.");
let hspace_px = pixels_per_point * 4.0;
let size_px = Vec2::new(2.0 * n as f32 + hspace_px, n as f32);
let size_points = size_px / pixels_per_point + Vec2::splat(2.0);
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
let mut cursor_px = Pos2::new(
response.rect.min.x * pixels_per_point,
response.rect.min.y * pixels_per_point,
)
.ceil();
// Vertical stripes:
for x in 0..n / 2 {
let rect_px = Rect::from_min_size(
Pos2::new(cursor_px.x + 2.0 * x as f32, cursor_px.y),
Vec2::new(1.0, n as f32),
);
painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE);
let rect_px = rect_px.translate(vec2(1.0, 0.0));
painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK);
}
cursor_px.x += n as f32 + hspace_px;
// Horizontal stripes:
for y in 0..n / 2 {
let rect_px = Rect::from_min_size(
Pos2::new(cursor_px.x, cursor_px.y + 2.0 * y as f32),
Vec2::new(n as f32, 1.0),
);
painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE);
let rect_px = rect_px.translate(vec2(0.0, 1.0));
painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK);
}
}
fn blending_and_feathering_test(ui: &mut Ui) {
ui.label("The left side shows how lines of different widths look.");
ui.label("The right side tests text rendering at different opacities and sizes.");
ui.label("The top and bottom images should look symmetrical in their intensities.");
let size = vec2(512.0, 512.0);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let rect = response.rect;
let mut top_half = rect;
top_half.set_bottom(top_half.center().y);
painter.rect_filled(top_half, 0.0, Color32::BLACK);
paint_fine_lines_and_text(&painter, top_half, Color32::WHITE);
let mut bottom_half = rect;
bottom_half.set_top(bottom_half.center().y);
painter.rect_filled(bottom_half, 0.0, Color32::WHITE);
paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK);
}
fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) {
assert!(fg.is_opaque());
assert!(bg.is_opaque());
ui.horizontal(|ui| {
ui.label(
RichText::from("▣ The quick brown fox jumps over the lazy dog and runs away.")
.background_color(bg)
.color(fg),
);
ui.label(format!(
"({} {} {}) on ({} {} {})",
fg.r(),
fg.g(),
fg.b(),
bg.r(),
bg.g(),
bg.b(),
));
});
}
fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) {
{
let mut y = 0.0;
for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] {
painter.text(
rect.center_top() + vec2(0.0, y),
Align2::LEFT_TOP,
format!("{:.0}% white", 100.0 * opacity),
FontId::proportional(14.0),
Color32::WHITE.gamma_multiply(opacity),
);
painter.text(
rect.center_top() + vec2(80.0, y),
Align2::LEFT_TOP,
format!("{:.0}% gray", 100.0 * opacity),
FontId::proportional(14.0),
Color32::GRAY.gamma_multiply(opacity),
);
painter.text(
rect.center_top() + vec2(160.0, y),
Align2::LEFT_TOP,
format!("{:.0}% black", 100.0 * opacity),
FontId::proportional(14.0),
Color32::BLACK.gamma_multiply(opacity),
);
y += 20.0;
}
for font_size in [6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0] {
painter.text(
rect.center_top() + vec2(0.0, y),
Align2::LEFT_TOP,
format!(
"{font_size}px - The quick brown fox jumps over the lazy dog and runs away."
),
FontId::proportional(font_size),
color,
);
y += font_size + 1.0;
}
}
rect.max.x = rect.center().x;
rect = rect.shrink(16.0);
for width in [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0] {
painter.text(
rect.left_top(),
Align2::CENTER_CENTER,
width.to_string(),
FontId::monospace(12.0),
color,
);
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(
[
rect.left_top() + vec2(16.0, 0.0),
rect.right_top(),
rect.right_center(),
rect.right_bottom(),
],
false,
Color32::TRANSPARENT,
Stroke::new(width, color),
));
rect.min.y += 24.0;
rect.max.x -= 24.0;
}
rect.min.y += 16.0;
painter.text(
rect.left_top(),
Align2::LEFT_CENTER,
"transparent --> opaque",
FontId::monospace(10.0),
color,
);
rect.min.y += 12.0;
let mut mesh = Mesh::default();
mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT);
mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT);
mesh.colored_vertex(rect.right_bottom(), color);
mesh.colored_vertex(rect.right_top(), color);
mesh.add_triangle(0, 1, 2);
mesh.add_triangle(1, 2, 3);
painter.add(mesh);
}
fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
Color32::from_rgba_premultiplied(
(left.r() as f32 * right.r() as f32 / 255.0).round() as u8,
(left.g() as f32 * right.g() as f32 / 255.0).round() as u8,
(left.b() as f32 * right.b() as f32 / 255.0).round() as u8,
(left.a() as f32 * right.a() as f32 / 255.0).round() as u8,
)
}