1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Rename AlphaFromCoverage to FontColorTransferFunction (#8201)

`ab_glyph` would output coverage values, but `vello` outputs RGBA. So
the old name was a misnomer.

I also suspect our default values are wrong, but I need to investigate
that more properly in a separate PR.
This commit is contained in:
Emil Ernerfeldt
2026-05-26 10:51:39 +02:00
committed by GitHub
parent 71f22ff1a5
commit fc1b2a99fd
6 changed files with 113 additions and 58 deletions

View File

@@ -1,3 +1,4 @@
use ecolor::linear_f32_from_linear_u8;
use emath::Vec2;
use crate::{Color32, textures::TextureOptions};
@@ -346,22 +347,37 @@ 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.
///
/// epaint stores all glyphs in the font atlas as white (with varying opacity),
/// so that egui can reuse the same glyph for different text colors
/// (with a simple color multiplication in the shader).
///
/// Because of this simplification, we need to apply a non-linear
/// ramp to the glyph colors before writing them into the font atlas,
/// as a way to compensate.
///
/// This whole thing is less than rigorous.
///
/// It would be better to either render all text colors into the font atlas
/// (which would require more atlas space, but would allow for more accurate rendering of colored text and emojis),
/// or do the color compensation in the shader, based on the active text color.
///
/// When experimenting, use <https://fonts.google.com/specimen/Ubuntu> to compare to a ground truth.
///
/// See <https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html> for related analysis.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum AlphaFromCoverage {
/// `alpha = coverage`.
pub enum FontColorTransferFunction {
/// Use the raw RGBA values from the font rasterizer, without any conversion.
///
/// Looks good for black-on-white text, i.e. light mode.
/// This is the required mode for colored emojis etc.
///
/// Same as [`Self::Gamma`]`(1.0)`, but more efficient.
Linear,
/// This mode looks good for black-on-white text, i.e. light mode.
Off,
/// `alpha = coverage^gamma`.
///
/// Gamma=1 looks good for black-on-white text, i.e. light mode.
Gamma(f32),
/// `alpha = 2 * coverage - coverage^2`
@@ -374,29 +390,59 @@ pub enum AlphaFromCoverage {
TwoCoverageMinusCoverageSq,
}
impl AlphaFromCoverage {
impl FontColorTransferFunction {
/// A good-looking default for light mode (black-on-white text).
pub const LIGHT_MODE_DEFAULT: Self = Self::Linear;
pub const LIGHT_MODE_DEFAULT: Self = Self::Off;
/// A good-looking default for dark mode (white-on-black text).
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
/// How to convert a white color written by the font rasterizer
/// into a color to be written into the font atlas.
#[inline(always)]
pub fn to_atlas_color(self, input_color: Color32) -> Color32 {
match self {
Self::Off | Self::Gamma(1.0) => input_color,
Self::Gamma(gamma) => {
let coverage = linear_f32_from_linear_u8(input_color.a());
let alpha = coverage.powf(gamma);
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
}
Self::TwoCoverageMinusCoverageSq => {
let coverage = linear_f32_from_linear_u8(input_color.a());
let alpha = 2.0 * coverage - coverage * coverage;
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
}
}
}
/// Convert coverage to alpha.
#[inline(always)]
pub fn alpha_from_coverage(&self, coverage: f32) -> f32 {
pub fn alpha_from_coverage(self, coverage: f32) -> f32 {
let coverage = coverage.clamp(0.0, 1.0);
match self {
Self::Linear => coverage,
Self::Gamma(gamma) => coverage.powf(*gamma),
Self::Off | Self::Gamma(1.0) => 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 {
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))
}
/// Convert this into the closest gamma exponent
pub fn to_gamma(self) -> f32 {
match self {
Self::Off => 1.0,
Self::Gamma(gamma) => gamma,
Self::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
}
}
}
// ----------------------------------------------------------------------------

View File

@@ -52,7 +52,7 @@ pub use self::{
corner_radius::CornerRadius,
corner_radius_f32::CornerRadiusF32,
direction::Direction,
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
image::{ColorImage, FontColorTransferFunction, ImageData, ImageDelta},
margin::Margin,
margin_f32::*,
mesh::{Mesh, Mesh16, Vertex},

View File

@@ -1,5 +1,6 @@
#![expect(clippy::mem_forget)]
use ecolor::Color32;
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
use self_cell::self_cell;
use skrifa::{GlyphId, MetadataProvider as _};
@@ -274,26 +275,31 @@ impl FontCell {
let width = bounds.width() as u16;
let height = bounds.height() as u16;
let mut ctx = vello_cpu::RenderContext::new(width, height);
ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
ctx.fill_path(&path);
let mut dest = vello_cpu::Pixmap::new(width, height);
let mut resources = vello_cpu::Resources::new();
ctx.render_to_pixmap(&mut resources, &mut dest);
let uv_rect = if width == 0 || height == 0 {
UvRect::default()
} else {
let mut ctx = vello_cpu::RenderContext::new(width, height);
ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
ctx.fill_path(&path);
let mut dest = vello_cpu::Pixmap::new(width, height);
let mut resources = vello_cpu::Resources::new();
ctx.render_to_pixmap(&mut resources, &mut dest);
let glyph_pos = {
let alpha_from_coverage = atlas.options().alpha_from_coverage;
let color_transfer_function = atlas.options().color_transfer_function;
let (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
let pixels = dest.data_as_u8_slice();
for y in 0..height as usize {
for x in 0..width as usize {
image[(x + glyph_pos.0, y + glyph_pos.1)] = alpha_from_coverage
.color_from_coverage(
pixels[((y * width as usize) + x) * 4 + 3] as f32 / 255.0,
);
let pixel_offset = 4 * ((y * width as usize) + x);
image[(x + glyph_pos.0, y + glyph_pos.1)] = color_transfer_function
.to_atlas_color(Color32::from_rgba_premultiplied(
pixels[pixel_offset],
pixels[pixel_offset + 1],
pixels[pixel_offset + 2],
pixels[pixel_offset + 3],
));
}
}
glyph_pos

View File

@@ -25,8 +25,8 @@ pub struct TextOptions {
/// Maximum size of the font texture.
pub max_texture_side: usize,
/// Controls how to convert glyph coverage to alpha.
pub alpha_from_coverage: crate::AlphaFromCoverage,
/// Controls how to convert glyph colors when writing to the font atlas.
pub color_transfer_function: crate::FontColorTransferFunction,
/// Whether to enable font hinting
///
@@ -54,7 +54,7 @@ impl Default for TextOptions {
fn default() -> Self {
Self {
max_texture_side: 2048, // Small but portable
alpha_from_coverage: crate::AlphaFromCoverage::default(),
color_transfer_function: crate::FontColorTransferFunction::default(),
font_hinting: true,
subpixel_binning: true,
}

View File

@@ -120,8 +120,9 @@ impl TextureAtlas {
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
let coverage =
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
options.alpha_from_coverage.color_from_coverage(coverage);
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = options
.color_transfer_function
.color_from_coverage(coverage);
}
}
atlas.discs.push(PrerasterizedDisc {