Browse Source

add color test window to troubleshoot your Egui painter backend

egui_glium passes the test, but the egui_web WebGL painter fails it.
pull/21/head
Emil Ernerfeldt 4 years ago
parent
commit
1069ad8496
  1. 16
      TODO.md
  2. 2
      egui/src/demos.rs
  3. 30
      egui/src/demos/app.rs
  4. 359
      egui/src/demos/color_test.rs
  5. 2
      egui/src/math/vec2.rs
  6. 26
      egui/src/paint/color.rs
  7. 45
      egui/src/paint/command.rs
  8. 2
      egui/src/ui.rs
  9. 14
      egui/src/widgets/color_picker.rs
  10. 20
      egui/src/widgets/image.rs
  11. 1
      egui_web/src/webgl.rs

16
TODO.md

@ -17,9 +17,10 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [x] Color edit button with popup color picker
* [x] Gamma for value (brightness) slider
* [x] Easily edit users own (s)RGBA quadruplets (`&mut [u8;4]`/`[f32;4]`)
* [ ] RGB editing without alpha
* [x] RGB editing without alpha
* [ ] Additive blending aware color picker
* [ ] Premultiplied alpha is a bit of a pain in the ass. Maybe rethink this a bit.
* [ ] Hue wheel
* Containers
* [ ] Scroll areas
* [x] Vertical scrolling
@ -53,19 +54,17 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [ ] Positioning preference: `window.preference(Top, Right)`
* [ ] Keeping right/bottom on expand. Maybe cover jitteryness with quick animation?
* [ ] Make auto-positioning of windows respect permanent side-bars.
* [/] Image support
* [x] Image support
* [x] Show user textures
* [ ] API for creating a texture managed by Egui
* Backend-agnostic. Good for people doing Egui-apps (games etc).
* [ ] Convert font texture to RGBA, or communicate format in initialization?
* [ ] Generalized font atlas
* [x] API for creating a texture managed by `egui::app::Backend`
* Visuals
* [x] Pixel-perfect painting (round positions to nearest pixel).
* [x] Fix `aa_size`: should be 1, currently fudged at 1.5
* [x] Fix thin rounded corners rendering bug (too bright)
* [x] Smoother animation (e.g. ease-out)? NO: animation are too brief for subtelty
* [ ] Veriy alpha and sRGB correctness
* [x] Veriy alpha and sRGB correctness
* [x] sRGBA decode in fragment shader
* [ ] Fix alpha blending / sRGB weirdness in WebGL (EXT_sRGB)
* [ ] Thin circles look bad
* [ ] Allow adding multiple tooltips to the same widget, showing them all one after the other.
* Math
@ -86,7 +85,8 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [x] Scroll input
* [x] Change to resize cursor on hover
* [x] Port most code to Rust
* [x] Read url fragment and redirect to a subpage (e.g. different examples apps)
* [x] Read url fragment and redirect to a subpage (e.g. different examples apps)]
* [ ] Fix WebGL colors/beldning (try EXT_sRGB)
* [ ] Embeddability
* [ ] Support canvas that does NOT cover entire screen.
* [ ] Support multiple eguis in one web page.

2
egui/src/demos.rs

@ -2,10 +2,12 @@
//!
//! The demo-code is also used in benchmarks and tests.
mod app;
mod color_test;
mod fractal_clock;
pub mod toggle_switch;
pub use {
app::{DemoApp, DemoWindow},
color_test::ColorTest,
fractal_clock::FractalClock,
};

30
egui/src/demos/app.rs

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{app, color::*, containers::*, demos::FractalClock, paint::*, widgets::*, *};
use crate::{app, color::*, containers::*, demos::*, paint::*, widgets::*, *};
// ----------------------------------------------------------------------------
@ -17,6 +17,9 @@ pub struct DemoApp {
demo_window: DemoWindow,
fractal_clock: FractalClock,
num_frames_painted: u64,
#[serde(skip)]
color_test: ColorTest,
show_color_test: bool,
}
impl DemoApp {
@ -189,6 +192,12 @@ impl DemoApp {
self.num_frames_painted += 1;
ui.label(format!("Total frames painted: {}", self.num_frames_painted));
ui.separator();
ui.checkbox(
"Show color blend test (debug backend painter)",
&mut self.show_color_test,
);
}
}
@ -198,6 +207,25 @@ impl app::App for DemoApp {
self.backend_ui(ui, backend);
});
let Self {
show_color_test,
color_test,
..
} = self;
if *show_color_test {
let mut tex_loader = |size: (usize, usize), pixels: &[Srgba]| {
backend.new_texture_srgba_premultiplied(size, pixels)
};
Window::new("Color Test")
.default_size(vec2(1024.0, 1024.0))
.scroll(true)
.open(show_color_test)
.show(ui.ctx(), |ui| {
color_test.ui(ui, &mut tex_loader);
});
}
let web_info = backend.web_info();
let web_location_hash = web_info
.as_ref()

359
egui/src/demos/color_test.rs

@ -0,0 +1,359 @@
use crate::widgets::color_picker::show_color;
use crate::*;
use color::*;
use std::collections::HashMap;
pub type TextureLoader<'a> = dyn FnMut((usize, usize), &[crate::Srgba]) -> TextureId + 'a;
const GRADIENT_SIZE: Vec2 = vec2(256.0, 24.0);
pub struct ColorTest {
tex_mngr: TextureManager,
vertex_gradients: bool,
texture_gradients: bool,
srgb: bool,
}
impl Default for ColorTest {
fn default() -> Self {
Self {
tex_mngr: Default::default(),
vertex_gradients: true,
texture_gradients: true,
srgb: false,
}
}
}
impl ColorTest {
pub fn ui(&mut self, ui: &mut Ui, tex_loader: &mut TextureLoader<'_>) {
ui.label("This is made to test if your Egui painter backend is set up correctly");
ui.label("It is meant to ensure you do proper sRGBA decoding of both texture and vertex colors, and blend using premultiplied alpha.");
ui.label("If everything is set up correctly, all groups of gradients will look uniform");
ui.checkbox("Vertex gradients", &mut self.vertex_gradients);
ui.checkbox("Texture gradients", &mut self.texture_gradients);
ui.checkbox("Show naive sRGBA horror", &mut self.srgb);
ui.heading("sRGB color test");
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
ui.add_custom(|ui| {
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
let g = Gradient::one_color(Srgba::new(255, 165, 0, 255));
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
self.tex_gradient(
ui,
tex_loader,
"orange rgb(255, 165, 0) - texture",
WHITE,
&g,
);
});
ui.separator();
ui.label("Test that vertex color times texture color is done in linear space:");
ui.add_custom(|ui| {
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
let tex_color = Rgba::new(1.0, 0.25, 0.25, 1.0);
let vertex_color = Rgba::new(0.5, 0.75, 0.75, 1.0);
ui.horizontal_centered(|ui| {
let color_size = vec2(2.0, 1.0) * ui.style().spacing.clickable_diameter;
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(Srgba::from(tex_color * vertex_color));
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
self.tex_gradient(ui, tex_loader, "Ground truth (texture)", WHITE, &g);
}
ui.horizontal_centered(|ui| {
let g = Gradient::one_color(Srgba::from(tex_color));
let tex = self.tex_mngr.get(tex_loader, &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::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.tooltip_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
});
ui.separator();
ui.separator();
// TODO: test color multiplication (image tint),
// to make sure vertex and texture color multiplication is done in linear space.
self.show_gradients(ui, tex_loader, WHITE, (RED, GREEN));
if self.srgb {
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
}
ui.separator();
self.show_gradients(ui, tex_loader, RED, (TRANSPARENT, GREEN));
ui.separator();
self.show_gradients(ui, tex_loader, WHITE, (TRANSPARENT, GREEN));
if self.srgb {
ui.label(
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
);
}
ui.separator();
// TODO: another ground truth where we do the alpha-blending against the background also.
// TODO: exactly the same thing, but with vertex colors (no textures)
self.show_gradients(ui, tex_loader, WHITE, (TRANSPARENT, BLACK));
ui.separator();
self.show_gradients(ui, tex_loader, BLACK, (TRANSPARENT, WHITE));
ui.separator();
ui.label("Additive blending: add more and more blue to the red background:");
self.show_gradients(ui, tex_loader, RED, (TRANSPARENT, Srgba::new(0, 0, 255, 0)));
ui.separator();
}
fn show_gradients(
&mut self,
ui: &mut Ui,
tex_loader: &mut TextureLoader<'_>,
bg_fill: Srgba,
(left, right): (Srgba, Srgba),
) {
let is_opaque = left.is_opaque() && right.is_opaque();
ui.horizontal_centered(|ui| {
let color_size = vec2(2.0, 1.0) * ui.style().spacing.clickable_diameter;
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.add_custom(|ui| {
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
if is_opaque {
let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
self.tex_gradient(
ui,
tex_loader,
"Ground Truth (CPU gradient) - texture",
bg_fill,
&g,
);
} else {
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
self.vertex_gradient(
ui,
"Ground Truth (CPU gradient, CPU blending) - vertices",
bg_fill,
&g,
);
self.tex_gradient(
ui,
tex_loader,
"Ground Truth (CPU gradient, CPU blending) - texture",
bg_fill,
&g,
);
let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
self.tex_gradient(
ui,
tex_loader,
"CPU gradient, GPU blending - texture",
bg_fill,
&g,
);
}
let g = Gradient::texture_gradient(left, right);
self.vertex_gradient(
ui,
"Triangle mesh of width 2 (test vertex decode and interpolation)",
bg_fill,
&g,
);
self.tex_gradient(
ui,
tex_loader,
"Texture of width 2 (test texture sampler)",
bg_fill,
&g,
);
if self.srgb {
let g =
Gradient::ground_truth_bad_srgba_gradient(left, right).with_bg_fill(bg_fill);
self.vertex_gradient(
ui,
"Triangle mesh with naive sRGBA interpolation (WRONG)",
bg_fill,
&g,
);
self.tex_gradient(
ui,
tex_loader,
"Naive sRGBA interpolation (WRONG)",
bg_fill,
&g,
);
}
});
}
fn tex_gradient(
&mut self,
ui: &mut Ui,
tex_loader: &mut TextureLoader<'_>,
label: &str,
bg_fill: Srgba,
gradient: &Gradient,
) {
if !self.texture_gradients {
return;
}
ui.horizontal_centered(|ui| {
let tex = self.tex_mngr.get(tex_loader, 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::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
.tooltip_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: Srgba, gradient: &Gradient) {
if !self.vertex_gradients {
return;
}
ui.horizontal_centered(|ui| {
vertex_gradient(ui, bg_fill, gradient).tooltip_text(format!(
"A triangle mesh that is {} vertices wide",
gradient.0.len()
));
ui.label(label);
});
}
}
fn vertex_gradient(ui: &mut Ui, bg_fill: Srgba, gradient: &Gradient) -> Response {
use crate::paint::*;
let rect = ui.allocate_space(GRADIENT_SIZE);
if bg_fill != Default::default() {
let mut triangles = Triangles::default();
triangles.add_colored_rect(rect, bg_fill);
ui.painter().add(PaintCmd::Triangles(triangles));
}
{
let n = gradient.0.len();
assert!(n >= 2);
let mut triangles = Triangles::default();
for (i, &color) in gradient.0.iter().enumerate() {
let t = i as f32 / (n as f32 - 1.0);
let x = lerp(rect.range_x(), t);
triangles.colored_vertex(pos2(x, rect.top()), color);
triangles.colored_vertex(pos2(x, rect.bottom()), color);
if i < n - 1 {
let i = i as u32;
triangles.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
triangles.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
}
}
ui.painter().add(PaintCmd::Triangles(triangles));
}
ui.interact_hover(rect)
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct Gradient(pub Vec<Srgba>);
impl Gradient {
pub fn one_color(srgba: Srgba) -> Self {
Self(vec![srgba, srgba])
}
pub fn texture_gradient(left: Srgba, right: Srgba) -> Self {
Self(vec![left, right])
}
pub fn ground_truth_linear_gradient(left: Srgba, right: Srgba) -> 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;
Srgba::from(lerp(left..=right, t))
})
.collect(),
)
}
/// This is how a bad person blends `sRGBA`
pub fn ground_truth_bad_srgba_gradient(left: Srgba, right: Srgba) -> Self {
let n = 255;
Self(
(0..=n)
.map(|i| {
let t = i as f32 / n as f32;
Srgba([
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, // Don't ever do this please!
])
})
.collect(),
)
}
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
pub fn with_bg_fill(self, bg: Srgba) -> Self {
let bg = Rgba::from(bg);
Self(
self.0
.into_iter()
.map(|fg| {
let fg = Rgba::from(fg);
Srgba::from(bg * (1.0 - fg.a()) + fg)
})
.collect(),
)
}
pub fn to_pixel_row(&self) -> Vec<Srgba> {
self.0.clone()
}
}
#[derive(Default)]
struct TextureManager(HashMap<Gradient, TextureId>);
impl TextureManager {
fn get(&mut self, tex_loader: &mut TextureLoader<'_>, gradient: &Gradient) -> TextureId {
*self.0.entry(gradient.clone()).or_insert_with(|| {
let pixels = gradient.to_pixel_row();
let width = pixels.len();
let height = 1;
tex_loader((width, height), &pixels)
})
}
}

2
egui/src/math/vec2.rs

@ -13,7 +13,7 @@ pub struct Vec2 {
}
#[inline(always)]
pub fn vec2(x: f32, y: f32) -> Vec2 {
pub const fn vec2(x: f32, y: f32) -> Vec2 {
Vec2 { x, y }
}

26
egui/src/paint/color.rs

@ -9,7 +9,7 @@ use crate::math::clamp;
/// Alpha channel is in linear space.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Srgba(pub [u8; 4]);
pub struct Srgba(pub(crate) [u8; 4]);
impl std::ops::Index<usize> for Srgba {
type Output = u8;
@ -49,9 +49,8 @@ impl Srgba {
Self([l, l, l, 0])
}
/// Returns an opaque version of self
pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into()
pub fn is_opaque(&self) -> bool {
self.a() == 255
}
pub fn r(&self) -> u8 {
@ -67,6 +66,11 @@ impl Srgba {
self.0[3]
}
/// Returns an opaque version of self
pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into()
}
pub fn to_array(&self) -> [u8; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
@ -94,7 +98,7 @@ pub const LIGHT_BLUE: Srgba = srgba(140, 160, 255, 255);
/// 0-1 linear space `RGBA` color with premultiplied alpha.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Rgba(pub [f32; 4]);
pub struct Rgba(pub(crate) [f32; 4]);
impl std::ops::Index<usize> for Rgba {
type Output = f32;
@ -200,6 +204,18 @@ impl std::ops::Add for Rgba {
}
}
impl std::ops::Mul<Rgba> for Rgba {
type Output = Rgba;
fn mul(self, other: Rgba) -> Rgba {
Rgba([
self[0] * other[0],
self[1] * other[1],
self[2] * other[2],
self[3] * other[3],
])
}
}
impl std::ops::Mul<f32> for Rgba {
type Output = Rgba;
fn mul(self, factor: f32) -> Rgba {

45
egui/src/paint/command.rs

@ -43,6 +43,51 @@ pub enum PaintCmd {
Triangles(Triangles),
}
impl PaintCmd {
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self {
Self::LineSegment {
points,
stroke: stroke.into(),
}
}
pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into<Srgba>) -> Self {
Self::Circle {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
}
}
pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
Self::Circle {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
}
}
pub fn rect_filled(rect: Rect, corner_radius: f32, fill_color: impl Into<Srgba>) -> Self {
Self::Rect {
rect,
corner_radius,
fill: fill_color.into(),
stroke: Default::default(),
}
}
pub fn rect_stroke(rect: Rect, corner_radius: f32, stroke: impl Into<Stroke>) -> Self {
Self::Rect {
rect,
corner_radius,
fill: Default::default(),
stroke: stroke.into(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Stroke {

2
egui/src/ui.rs

@ -578,7 +578,7 @@ impl Ui {
self.allocate_space(child_ui.bounding_size())
}
/// Create a child ui
/// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance.
pub fn add_custom<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
let child_rect = self.available();
let mut child_ui = self.child_ui(child_rect);

14
egui/src/widgets/color_picker.rs

@ -38,14 +38,18 @@ fn background_checkers(painter: &Painter, rect: Rect) {
painter.add(PaintCmd::Triangles(triangles));
}
fn show_color(ui: &mut Ui, color: Srgba, desired_size: Vec2) -> Response {
pub fn show_color(ui: &mut Ui, color: impl Into<Srgba>, desired_size: Vec2) -> Response {
show_srgba(ui, color.into(), desired_size)
}
fn show_srgba(ui: &mut Ui, srgba: Srgba, desired_size: Vec2) -> Response {
let rect = ui.allocate_space(desired_size);
background_checkers(ui.painter(), rect);
ui.painter().add(PaintCmd::Rect {
rect,
corner_radius: 2.0,
fill: color,
stroke: Stroke::new(3.0, color.to_opaque()),
fill: srgba,
stroke: Stroke::new(3.0, srgba.to_opaque()),
});
ui.interact_hover(rect)
}
@ -191,9 +195,9 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma) {
ui.style().spacing.clickable_diameter * 2.0,
);
show_color(ui, (*hsva).into(), current_color_size).tooltip_text("Current color");
show_color(ui, *hsva, current_color_size).tooltip_text("Current color");
show_color(ui, HsvaGamma { a: 1.0, ..*hsva }.into(), current_color_size)
show_color(ui, HsvaGamma { a: 1.0, ..*hsva }, current_color_size)
.tooltip_text("Current color (opaque)");
let opaque = HsvaGamma { a: 1.0, ..*hsva };

20
egui/src/widgets/image.rs

@ -3,6 +3,7 @@ use crate::*;
#[derive(Clone, Copy, Debug, Default)]
pub struct Image {
texture_id: TextureId,
uv: Rect,
desired_size: Vec2,
bg_fill: Srgba,
tint: Srgba,
@ -12,21 +13,28 @@ impl Image {
pub fn new(texture_id: TextureId, desired_size: Vec2) -> Self {
Self {
texture_id,
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
desired_size,
tint: color::WHITE,
..Default::default()
}
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.uv = uv.into();
self
}
/// A solid color to put behind the image. Useful for transparent images.
pub fn bg_fill(mut self, bg_fill: Srgba) -> Self {
self.bg_fill = bg_fill;
pub fn bg_fill(mut self, bg_fill: impl Into<Srgba>) -> Self {
self.bg_fill = bg_fill.into();
self
}
/// Multiply image color with this. Default is WHITE (no tint).
pub fn tint(mut self, tint: Srgba) -> Self {
self.tint = tint;
pub fn tint(mut self, tint: impl Into<Srgba>) -> Self {
self.tint = tint.into();
self
}
}
@ -36,6 +44,7 @@ impl Widget for Image {
use paint::*;
let Self {
texture_id,
uv,
desired_size,
bg_fill,
tint,
@ -48,9 +57,8 @@ impl Widget for Image {
}
{
// TODO: builder pattern for Triangles
let uv = [pos2(0.0, 0.0), pos2(1.0, 1.0)];
let mut triangles = Triangles::with_texture(texture_id);
triangles.add_rect_with_uv(rect, uv.into(), tint);
triangles.add_rect_with_uv(rect, uv, tint);
ui.painter().add(PaintCmd::Triangles(triangles));
}

1
egui_web/src/webgl.rs

@ -207,6 +207,7 @@ impl Painter {
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
// TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB
// https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/
let level = 0;
let internal_format = Gl::RGBA;
let border = 0;

Loading…
Cancel
Save