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
This commit is contained in:
Emil Ernerfeldt 2022-03-23 16:49:49 +01:00 committed by GitHub
parent d3c002a4e5
commit 8272b08742
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 57 additions and 55 deletions

View file

@ -16,10 +16,12 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
### Changed 🔧 ### Changed 🔧
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).
* Renamed `Frame::margin` to `Frame::inner_margin`. * 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 🐛
* Fixed ComboBoxes always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)). * 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)). * 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 🔥
* Removed the `single_threaded/multi_threaded` flags - egui is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)). * Removed the `single_threaded/multi_threaded` flags - egui is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)).

View file

@ -17,7 +17,7 @@ impl Default for WrappedTextureManager {
// Will be filled in later // Will be filled in later
let font_id = tex_mngr.alloc( let font_id = tex_mngr.alloc(
"egui_font_texture".into(), "egui_font_texture".into(),
epaint::AlphaImage::new([0, 0]).into(), epaint::FontImage::new([0, 0]).into(),
); );
assert_eq!(font_id, TextureId::default()); assert_eq!(font_id, TextureId::default());

View file

@ -307,7 +307,7 @@ pub use epaint::{
color, mutex, color, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::TexturesDelta, textures::TexturesDelta,
AlphaImage, ClippedPrimitive, Color32, ColorImage, ImageData, Mesh, PaintCallback, ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback,
PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId, PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId,
}; };

View file

@ -235,7 +235,7 @@ impl Painter {
); );
image.pixels.iter().map(|color| color.to_tuple()).collect() image.pixels.iter().map(|color| color.to_tuple()).collect()
} }
egui::ImageData::Alpha(image) => { egui::ImageData::Font(image) => {
let gamma = 1.0; let gamma = 1.0;
image image
.srgba_pixels(gamma) .srgba_pixels(gamma)

View file

@ -478,7 +478,7 @@ impl Painter {
self.upload_texture_srgb(delta.pos, image.size, data); self.upload_texture_srgb(delta.pos, image.size, data);
} }
egui::ImageData::Alpha(image) => { egui::ImageData::Font(image) => {
assert_eq!( assert_eq!(
image.width() * image.height(), image.width() * image.height(),
image.pixels.len(), image.pixels.len(),

View file

@ -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)). * 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)). * `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 `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 ## 0.17.0 - 2022-02-22

View file

@ -6,21 +6,21 @@ use crate::Color32;
/// ///
/// In order to paint the image on screen, you first need to convert it to /// 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)] #[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData { pub enum ImageData {
/// RGBA image. /// RGBA image.
Color(ColorImage), Color(ColorImage),
/// Used for the font texture. /// Used for the font texture.
Alpha(AlphaImage), Font(FontImage),
} }
impl ImageData { impl ImageData {
pub fn size(&self) -> [usize; 2] { pub fn size(&self) -> [usize; 2] {
match self { match self {
Self::Color(image) => image.size, 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 { pub fn bytes_per_pixel(&self) -> usize {
match self { match self {
Self::Color(_) => 4, Self::Color(_) => 4,
Self::Alpha(_) => 1, Self::Font(_) => 4,
} }
} }
} }
@ -157,25 +157,28 @@ impl From<ColorImage> 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 /// Each value represents "coverage", i.e. how much a texel is covered by a character.
#[derive(Clone, Default, Eq, Hash, PartialEq)] ///
/// This is roughly interpreted as the opacity of a white image.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct AlphaImage { pub struct FontImage {
/// width, height /// width, height
pub size: [usize; 2], 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. /// Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<u8>, pub pixels: Vec<f32>,
} }
impl AlphaImage { impl FontImage {
pub fn new(size: [usize; 2]) -> Self { pub fn new(size: [usize; 2]) -> Self {
Self { Self {
size, 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. /// `gamma` should normally be set to 1.0.
/// If you are having problems with text looking skinny and pixelated, try /// If you are having problems with text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`. /// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels( pub fn srgba_pixels(&'_ self, gamma: f32) -> impl ExactSizeIterator<Item = Color32> + '_ {
&'_ self, self.pixels.iter().map(move |coverage| {
gamma: f32, // This is arbitrarily chosen to make text look as good as possible.
) -> impl ExactSizeIterator<Item = super::Color32> + '_ { // In particular, it looks good with gamma=1 and the default eframe backend,
let srgba_from_alpha_lut: Vec<Color32> = (0..=255) // which uses linear blending.
.map(|a| { // See https://github.com/emilk/egui/issues/1410
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma); let a = fast_round(coverage.powf(gamma / 2.2) * 255.0);
super::Rgba::from_white_alpha(a).into() Color32::from_rgba_premultiplied(a, a, a, a) // this makes no sense, but works
}) })
.collect();
self.pixels
.iter()
.map(move |&a| srgba_from_alpha_lut[a as usize])
} }
/// Clone a sub-region as a new image /// Clone a sub-region as a new image.
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> AlphaImage { pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> FontImage {
assert!(x + w <= self.width()); assert!(x + w <= self.width());
assert!(y + h <= self.height()); assert!(y + h <= self.height());
@ -221,40 +219,44 @@ impl AlphaImage {
pixels.extend(&self.pixels[offset..(offset + w)]); pixels.extend(&self.pixels[offset..(offset + w)]);
} }
assert_eq!(pixels.len(), w * h); assert_eq!(pixels.len(), w * h);
AlphaImage { FontImage {
size: [w, h], size: [w, h],
pixels, pixels,
} }
} }
} }
impl std::ops::Index<(usize, usize)> for AlphaImage { impl std::ops::Index<(usize, usize)> for FontImage {
type Output = u8; type Output = f32;
#[inline] #[inline]
fn index(&self, (x, y): (usize, usize)) -> &u8 { fn index(&self, (x, y): (usize, usize)) -> &f32 {
let [w, h] = self.size; let [w, h] = self.size;
assert!(x < w && y < h); assert!(x < w && y < h);
&self.pixels[y * w + x] &self.pixels[y * w + x]
} }
} }
impl std::ops::IndexMut<(usize, usize)> for AlphaImage { impl std::ops::IndexMut<(usize, usize)> for FontImage {
#[inline] #[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; let [w, h] = self.size;
assert!(x < w && y < h); assert!(x < w && y < h);
&mut self.pixels[y * w + x] &mut self.pixels[y * w + x]
} }
} }
impl From<AlphaImage> for ImageData { impl From<FontImage> for ImageData {
#[inline(always)] #[inline(always)]
fn from(image: AlphaImage) -> Self { fn from(image: FontImage) -> Self {
Self::Alpha(image) 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. /// A change to an image.

View file

@ -28,7 +28,7 @@ pub mod util;
pub use { pub use {
bezier::{CubicBezierShape, QuadraticBezierShape}, bezier::{CubicBezierShape, QuadraticBezierShape},
color::{Color32, Rgba}, color::{Color32, Rgba},
image::{AlphaImage, ColorImage, ImageData, ImageDelta}, image::{ColorImage, FontImage, ImageData, ImageDelta},
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow, shadow::Shadow,
shape::{ shape::{

View file

@ -377,7 +377,7 @@ fn allocate_glyph(
if v > 0.0 { if v > 0.0 {
let px = glyph_pos.0 + x as usize; let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y 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, uv_rect,
} }
} }
fn fast_round(r: f32) -> u8 {
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
}

View file

@ -539,7 +539,7 @@ impl FontsImpl {
// Make the top left pixel fully white: // Make the top left pixel fully white:
let (pos, image) = atlas.allocate((1, 1)); let (pos, image) = atlas.allocate((1, 1));
assert_eq!(pos, (0, 0)); assert_eq!(pos, (0, 0));
image[pos] = 255; image[pos] = 1.0;
} }
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));

View file

@ -1,4 +1,4 @@
use crate::{AlphaImage, ImageDelta}; use crate::{FontImage, ImageDelta};
#[derive(Clone, Copy, Eq, PartialEq)] #[derive(Clone, Copy, Eq, PartialEq)]
struct Rectu { struct Rectu {
@ -32,7 +32,7 @@ impl Rectu {
/// More characters can be added, possibly expanding the texture. /// More characters can be added, possibly expanding the texture.
#[derive(Clone)] #[derive(Clone)]
pub struct TextureAtlas { pub struct TextureAtlas {
image: AlphaImage, image: FontImage,
/// What part of the image that is dirty /// What part of the image that is dirty
dirty: Rectu, dirty: Rectu,
@ -48,7 +48,7 @@ impl TextureAtlas {
pub fn new(size: [usize; 2]) -> Self { pub fn new(size: [usize; 2]) -> Self {
assert!(size[0] >= 1024, "Tiny texture atlas"); assert!(size[0] >= 1024, "Tiny texture atlas");
Self { Self {
image: AlphaImage::new(size), image: FontImage::new(size),
dirty: Rectu::EVERYTHING, dirty: Rectu::EVERYTHING,
cursor: (0, 0), cursor: (0, 0),
row_height: 0, row_height: 0,
@ -91,7 +91,7 @@ impl TextureAtlas {
/// Returns the coordinates of where the rect ended up, /// Returns the coordinates of where the rect ended up,
/// and invalidates the region. /// 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 /// On some low-precision GPUs (my old iPad) characters get muddled up
/// if we don't add some empty pixels between the characters. /// if we don't add some empty pixels between the characters.
/// On modern high-precision GPUs this is not needed. /// 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() { while required_height >= image.height() {
image.size[1] *= 2; // double the height image.size[1] *= 2; // double the height
} }
if image.width() * image.height() > image.pixels.len() { 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 true
} else { } else {
false false