mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Improve text rendering in light mode (#7290)
This changes how we convert glyph coverage to alpha (and ultimately a color), but only in light mode. This is a bit of a hack, because it doesn't fix dark-on-light text in _dark mode_ (if you have any), but for the common case this PR is a huge improvement. You can also tweak this yourself now using `Visuals::text_alpha_from_coverage` or from the UI (bottom of the image):  ## Before / After   ## Black text Before/after If you think the text above looks too weak, it's only because of the default text color. Here's how it looks like with perfectly `#000000` black text:  
This commit is contained in:
@@ -318,6 +318,59 @@ impl std::fmt::Debug for ColorImage {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to convert font coverage values into alpha and color values.
|
||||
//
|
||||
// This whole thing is less than rigorous.
|
||||
// Ideally we should do this in a shader instead, and use different computations
|
||||
// for different text colors.
|
||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum AlphaFromCoverage {
|
||||
/// `alpha = coverage`.
|
||||
///
|
||||
/// Looks good for black-on-white text, i.e. light mode.
|
||||
///
|
||||
/// Same as [`Self::Gamma`]`(1.0)`, but more efficient.
|
||||
Linear,
|
||||
|
||||
/// `alpha = coverage^gamma`.
|
||||
Gamma(f32),
|
||||
|
||||
/// `alpha = 2 * coverage - coverage^2`
|
||||
///
|
||||
/// This looks good for white-on-black text, i.e. dark mode.
|
||||
///
|
||||
/// Very similar to a gamma of 0.5, but produces sharper text.
|
||||
/// See <https://www.desmos.com/calculator/w0ndf5blmn> for a comparison to gamma=0.5.
|
||||
#[default]
|
||||
TwoCoverageMinusCoverageSq,
|
||||
}
|
||||
|
||||
impl AlphaFromCoverage {
|
||||
/// A good-looking default for light mode (black-on-white text).
|
||||
pub const LIGHT_MODE_DEFAULT: Self = Self::Linear;
|
||||
|
||||
/// A good-looking default for dark mode (white-on-black text).
|
||||
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
|
||||
|
||||
/// Convert coverage to alpha.
|
||||
#[inline(always)]
|
||||
pub fn alpha_from_coverage(&self, coverage: f32) -> f32 {
|
||||
match self {
|
||||
Self::Linear => coverage,
|
||||
Self::Gamma(gamma) => coverage.powf(*gamma),
|
||||
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn color_from_coverage(&self, coverage: f32) -> Color32 {
|
||||
let alpha = self.alpha_from_coverage(coverage);
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
}
|
||||
}
|
||||
|
||||
/// A single-channel image designed for the font texture.
|
||||
///
|
||||
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
||||
@@ -354,30 +407,21 @@ impl FontImage {
|
||||
}
|
||||
|
||||
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
||||
///
|
||||
/// `gamma` should normally be set to `None`.
|
||||
///
|
||||
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
|
||||
#[inline]
|
||||
pub fn srgba_pixels(&self, gamma: Option<f32>) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
||||
// This whole function is less than rigorous.
|
||||
// Ideally we should do this in a shader instead, and use different computations
|
||||
// for different text colors.
|
||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||
self.pixels.iter().map(move |coverage| {
|
||||
let alpha = if let Some(gamma) = gamma {
|
||||
coverage.powf(gamma)
|
||||
} else {
|
||||
// alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending)
|
||||
pub fn srgba_pixels(
|
||||
&self,
|
||||
alpha_from_coverage: AlphaFromCoverage,
|
||||
) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
||||
self.pixels
|
||||
.iter()
|
||||
.map(move |&coverage| alpha_from_coverage.color_from_coverage(coverage))
|
||||
}
|
||||
|
||||
// The following is recommended by the article for BLACK text (using linear blending).
|
||||
// Very similar to a gamma of 0.5, but produces sharper text.
|
||||
// In practice it works well for all text colors (better than a gamma of 0.5, for instance).
|
||||
// See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison.
|
||||
2.0 * coverage - coverage * coverage
|
||||
};
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
})
|
||||
/// Convert this coverage image to a [`ColorImage`].
|
||||
pub fn to_color_image(&self, alpha_from_coverage: AlphaFromCoverage) -> ColorImage {
|
||||
profiling::function_scope!();
|
||||
let pixels = self.srgba_pixels(alpha_from_coverage).collect();
|
||||
ColorImage::new(self.size, pixels)
|
||||
}
|
||||
|
||||
/// Clone a sub-region as a new image.
|
||||
|
||||
@@ -50,7 +50,7 @@ pub use self::{
|
||||
color::ColorMode,
|
||||
corner_radius::CornerRadius,
|
||||
corner_radius_f32::CornerRadiusF32,
|
||||
image::{ColorImage, FontImage, ImageData, ImageDelta},
|
||||
image::{AlphaFromCoverage, ColorImage, FontImage, ImageData, ImageDelta},
|
||||
margin::Margin,
|
||||
margin_f32::*,
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
|
||||
Reference in New Issue
Block a user