From 8272b08742088141217720f45f2dea8211321bd7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 23 Mar 2022 16:49:49 +0100 Subject: [PATCH] Improve text contrast in bright mode (#1412) * Rename AlphaImage to FontImage to discourage any other use for it * Encode FontImage as f32 and postpone the alpha correction * Interpret alpha coverage in a new, making dark text darker, improving contrast in bright mode --- CHANGELOG.md | 2 + egui/src/context.rs | 2 +- egui/src/lib.rs | 2 +- egui_glium/src/painter.rs | 2 +- egui_glow/src/painter.rs | 2 +- epaint/CHANGELOG.md | 2 + epaint/src/image.rs | 78 +++++++++++++++++++------------------ epaint/src/lib.rs | 2 +- epaint/src/text/font.rs | 6 +-- epaint/src/text/fonts.rs | 2 +- epaint/src/texture_atlas.rs | 12 +++--- 11 files changed, 57 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dbbcd0c..4965ec97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,12 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ### Changed 🔧 * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). * Renamed `Frame::margin` to `Frame::inner_margin`. +* Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)). ### Fixed 🐛 * Fixed ComboBoxes always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)). * Fixed ui code that could lead to a deadlock ([#1380](https://github.com/emilk/egui/pull/1380)). +* Text is darker and more readable in bright mode ([#1412](https://github.com/emilk/egui/pull/1412)). ### Removed 🔥 * Removed the `single_threaded/multi_threaded` flags - egui is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)). diff --git a/egui/src/context.rs b/egui/src/context.rs index 6666ba33..bb4fe880 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -17,7 +17,7 @@ impl Default for WrappedTextureManager { // Will be filled in later let font_id = tex_mngr.alloc( "egui_font_texture".into(), - epaint::AlphaImage::new([0, 0]).into(), + epaint::FontImage::new([0, 0]).into(), ); assert_eq!(font_id, TextureId::default()); diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 2a097849..24c9de4b 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -307,7 +307,7 @@ pub use epaint::{ color, mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::TexturesDelta, - AlphaImage, ClippedPrimitive, Color32, ColorImage, ImageData, Mesh, PaintCallback, + ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId, }; diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index 11d45fff..cbc74496 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -235,7 +235,7 @@ impl Painter { ); image.pixels.iter().map(|color| color.to_tuple()).collect() } - egui::ImageData::Alpha(image) => { + egui::ImageData::Font(image) => { let gamma = 1.0; image .srgba_pixels(gamma) diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index ebbf94ce..e26e86a1 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -478,7 +478,7 @@ impl Painter { self.upload_texture_srgb(delta.pos, image.size, data); } - egui::ImageData::Alpha(image) => { + egui::ImageData::Font(image) => { assert_eq!( image.width() * image.height(), image.pixels.len(), diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index de93bc15..25bce292 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to the epaint crate will be documented in this file. * Removed the `single_threaded/multi_threaded` flags - epaint is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)). * `Tessellator::from_options` is now `Tessellator::new` ([#1408](https://github.com/emilk/egui/pull/1408)). * Renamed `TessellationOptions::anti_alias` to `feathering` ([#1408](https://github.com/emilk/egui/pull/1408)). +* Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)). +* Dark text is darker and more readable on bright backgrounds ([#1412](https://github.com/emilk/egui/pull/1412)). ## 0.17.0 - 2022-02-22 diff --git a/epaint/src/image.rs b/epaint/src/image.rs index c261d924..2cfa586a 100644 --- a/epaint/src/image.rs +++ b/epaint/src/image.rs @@ -6,21 +6,21 @@ use crate::Color32; /// /// In order to paint the image on screen, you first need to convert it to /// -/// See also: [`ColorImage`], [`AlphaImage`]. +/// See also: [`ColorImage`], [`FontImage`]. #[derive(Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageData { /// RGBA image. Color(ColorImage), /// Used for the font texture. - Alpha(AlphaImage), + Font(FontImage), } impl ImageData { pub fn size(&self) -> [usize; 2] { match self { Self::Color(image) => image.size, - Self::Alpha(image) => image.size, + Self::Font(image) => image.size, } } @@ -35,7 +35,7 @@ impl ImageData { pub fn bytes_per_pixel(&self) -> usize { match self { Self::Color(_) => 4, - Self::Alpha(_) => 1, + Self::Font(_) => 4, } } } @@ -157,25 +157,28 @@ impl From for ImageData { // ---------------------------------------------------------------------------- -/// An 8-bit image, representing difference levels of transparent white. +/// A single-channel image designed for the font texture. /// -/// Used for the font texture -#[derive(Clone, Default, Eq, Hash, PartialEq)] +/// Each value represents "coverage", i.e. how much a texel is covered by a character. +/// +/// This is roughly interpreted as the opacity of a white image. +#[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct AlphaImage { +pub struct FontImage { /// width, height pub size: [usize; 2], - /// The alpha (linear space 0-255) of something white. + + /// The coverage value. /// - /// One byte per pixel. Often you want to use [`Self::srgba_pixels`] instead. - pub pixels: Vec, + /// Often you want to use [`Self::srgba_pixels`] instead. + pub pixels: Vec, } -impl AlphaImage { +impl FontImage { pub fn new(size: [usize; 2]) -> Self { Self { size, - pixels: vec![0; size[0] * size[1]], + pixels: vec![0.0; size[0] * size[1]], } } @@ -194,24 +197,19 @@ impl AlphaImage { /// `gamma` should normally be set to 1.0. /// If you are having problems with text looking skinny and pixelated, try /// setting a lower gamma, e.g. `0.5`. - pub fn srgba_pixels( - &'_ self, - gamma: f32, - ) -> impl ExactSizeIterator + '_ { - let srgba_from_alpha_lut: Vec = (0..=255) - .map(|a| { - let a = super::color::linear_f32_from_linear_u8(a).powf(gamma); - super::Rgba::from_white_alpha(a).into() - }) - .collect(); - - self.pixels - .iter() - .map(move |&a| srgba_from_alpha_lut[a as usize]) + pub fn srgba_pixels(&'_ self, gamma: f32) -> impl ExactSizeIterator + '_ { + self.pixels.iter().map(move |coverage| { + // This is arbitrarily chosen to make text look as good as possible. + // In particular, it looks good with gamma=1 and the default eframe backend, + // which uses linear blending. + // See https://github.com/emilk/egui/issues/1410 + let a = fast_round(coverage.powf(gamma / 2.2) * 255.0); + Color32::from_rgba_premultiplied(a, a, a, a) // this makes no sense, but works + }) } - /// Clone a sub-region as a new image - pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> AlphaImage { + /// Clone a sub-region as a new image. + pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> FontImage { assert!(x + w <= self.width()); assert!(y + h <= self.height()); @@ -221,40 +219,44 @@ impl AlphaImage { pixels.extend(&self.pixels[offset..(offset + w)]); } assert_eq!(pixels.len(), w * h); - AlphaImage { + FontImage { size: [w, h], pixels, } } } -impl std::ops::Index<(usize, usize)> for AlphaImage { - type Output = u8; +impl std::ops::Index<(usize, usize)> for FontImage { + type Output = f32; #[inline] - fn index(&self, (x, y): (usize, usize)) -> &u8 { + fn index(&self, (x, y): (usize, usize)) -> &f32 { let [w, h] = self.size; assert!(x < w && y < h); &self.pixels[y * w + x] } } -impl std::ops::IndexMut<(usize, usize)> for AlphaImage { +impl std::ops::IndexMut<(usize, usize)> for FontImage { #[inline] - fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 { + fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 { let [w, h] = self.size; assert!(x < w && y < h); &mut self.pixels[y * w + x] } } -impl From for ImageData { +impl From for ImageData { #[inline(always)] - fn from(image: AlphaImage) -> Self { - Self::Alpha(image) + fn from(image: FontImage) -> Self { + Self::Font(image) } } +fn fast_round(r: f32) -> u8 { + (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 +} + // ---------------------------------------------------------------------------- /// A change to an image. diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index ab559f89..3c642297 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -28,7 +28,7 @@ pub mod util; pub use { bezier::{CubicBezierShape, QuadraticBezierShape}, color::{Color32, Rgba}, - image::{AlphaImage, ColorImage, ImageData, ImageDelta}, + image::{ColorImage, FontImage, ImageData, ImageDelta}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shape::{ diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index d2f29267..9e35a5b1 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -377,7 +377,7 @@ fn allocate_glyph( if v > 0.0 { let px = glyph_pos.0 + x as usize; let py = glyph_pos.1 + y as usize; - image[(px, py)] = fast_round(v * 255.0); + image[(px, py)] = v; } }); @@ -405,7 +405,3 @@ fn allocate_glyph( uv_rect, } } - -fn fast_round(r: f32) -> u8 { - (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 -} diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index 716100eb..eb950fdf 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -539,7 +539,7 @@ impl FontsImpl { // Make the top left pixel fully white: let (pos, image) = atlas.allocate((1, 1)); assert_eq!(pos, (0, 0)); - image[pos] = 255; + image[pos] = 1.0; } let atlas = Arc::new(Mutex::new(atlas)); diff --git a/epaint/src/texture_atlas.rs b/epaint/src/texture_atlas.rs index c31625f3..0b8fe4dd 100644 --- a/epaint/src/texture_atlas.rs +++ b/epaint/src/texture_atlas.rs @@ -1,4 +1,4 @@ -use crate::{AlphaImage, ImageDelta}; +use crate::{FontImage, ImageDelta}; #[derive(Clone, Copy, Eq, PartialEq)] struct Rectu { @@ -32,7 +32,7 @@ impl Rectu { /// More characters can be added, possibly expanding the texture. #[derive(Clone)] pub struct TextureAtlas { - image: AlphaImage, + image: FontImage, /// What part of the image that is dirty dirty: Rectu, @@ -48,7 +48,7 @@ impl TextureAtlas { pub fn new(size: [usize; 2]) -> Self { assert!(size[0] >= 1024, "Tiny texture atlas"); Self { - image: AlphaImage::new(size), + image: FontImage::new(size), dirty: Rectu::EVERYTHING, cursor: (0, 0), row_height: 0, @@ -91,7 +91,7 @@ impl TextureAtlas { /// Returns the coordinates of where the rect ended up, /// and invalidates the region. - pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut AlphaImage) { + pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) { /// On some low-precision GPUs (my old iPad) characters get muddled up /// if we don't add some empty pixels between the characters. /// On modern high-precision GPUs this is not needed. @@ -138,13 +138,13 @@ impl TextureAtlas { } } -fn resize_to_min_height(image: &mut AlphaImage, required_height: usize) -> bool { +fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool { while required_height >= image.height() { image.size[1] *= 2; // double the height } if image.width() * image.height() > image.pixels.len() { - image.pixels.resize(image.width() * image.height(), 0); + image.pixels.resize(image.width() * image.height(), 0.0); true } else { false