mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -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:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{
|
use epaint::{
|
||||||
AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions,
|
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
text::{FontTweak, Tag},
|
text::{FontTweak, Tag},
|
||||||
};
|
};
|
||||||
@@ -1461,7 +1461,7 @@ impl Visuals {
|
|||||||
Self {
|
Self {
|
||||||
dark_mode: true,
|
dark_mode: true,
|
||||||
text_options: TextOptions {
|
text_options: TextOptions {
|
||||||
alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
|
color_transfer_function: FontColorTransferFunction::DARK_MODE_DEFAULT,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
override_text_color: None,
|
override_text_color: None,
|
||||||
@@ -1527,7 +1527,7 @@ impl Visuals {
|
|||||||
Self {
|
Self {
|
||||||
dark_mode: false,
|
dark_mode: false,
|
||||||
text_options: TextOptions {
|
text_options: TextOptions {
|
||||||
alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
|
color_transfer_function: FontColorTransferFunction::LIGHT_MODE_DEFAULT,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
widgets: Widgets::light(),
|
widgets: Widgets::light(),
|
||||||
@@ -2318,12 +2318,12 @@ impl Visuals {
|
|||||||
|
|
||||||
let TextOptions {
|
let TextOptions {
|
||||||
max_texture_side: _,
|
max_texture_side: _,
|
||||||
alpha_from_coverage,
|
color_transfer_function,
|
||||||
font_hinting,
|
font_hinting,
|
||||||
subpixel_binning,
|
subpixel_binning,
|
||||||
} = text_options;
|
} = text_options;
|
||||||
|
|
||||||
text_alpha_from_coverage_ui(ui, alpha_from_coverage);
|
color_transfer_function_ui(ui, color_transfer_function);
|
||||||
|
|
||||||
ui.checkbox(font_hinting, "Font hinting (sharper text)");
|
ui.checkbox(font_hinting, "Font hinting (sharper text)");
|
||||||
ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
|
ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
|
||||||
@@ -2437,23 +2437,29 @@ impl Visuals {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_alpha_from_coverage_ui(ui: &mut Ui, alpha_from_coverage: &mut AlphaFromCoverage) {
|
fn color_transfer_function_ui(
|
||||||
let mut dark_mode_special =
|
ui: &mut Ui,
|
||||||
*alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
|
color_transfer_function: &mut FontColorTransferFunction,
|
||||||
|
) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Text rendering:");
|
ui.label("Opacity tweaking:");
|
||||||
|
|
||||||
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
|
ui.radio_value(
|
||||||
|
color_transfer_function,
|
||||||
|
FontColorTransferFunction::Off,
|
||||||
|
"Off",
|
||||||
|
);
|
||||||
|
ui.radio_value(
|
||||||
|
color_transfer_function,
|
||||||
|
FontColorTransferFunction::DARK_MODE_DEFAULT,
|
||||||
|
"Dark-mode special",
|
||||||
|
);
|
||||||
|
|
||||||
if dark_mode_special {
|
let mut use_gamma = matches!(color_transfer_function, FontColorTransferFunction::Gamma(_));
|
||||||
*alpha_from_coverage = AlphaFromCoverage::DARK_MODE_DEFAULT;
|
ui.radio_value(&mut use_gamma, true, "Gamma function");
|
||||||
} else {
|
|
||||||
let mut gamma = match alpha_from_coverage {
|
if use_gamma {
|
||||||
AlphaFromCoverage::Linear => 1.0,
|
let mut gamma = color_transfer_function.to_gamma();
|
||||||
AlphaFromCoverage::Gamma(gamma) => *gamma,
|
|
||||||
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut gamma)
|
DragValue::new(&mut gamma)
|
||||||
@@ -2462,11 +2468,7 @@ fn text_alpha_from_coverage_ui(ui: &mut Ui, alpha_from_coverage: &mut AlphaFromC
|
|||||||
.prefix("Gamma: "),
|
.prefix("Gamma: "),
|
||||||
);
|
);
|
||||||
|
|
||||||
if gamma == 1.0 {
|
*color_transfer_function = FontColorTransferFunction::Gamma(gamma);
|
||||||
*alpha_from_coverage = AlphaFromCoverage::Linear;
|
|
||||||
} else {
|
|
||||||
*alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use ecolor::linear_f32_from_linear_u8;
|
||||||
use emath::Vec2;
|
use emath::Vec2;
|
||||||
|
|
||||||
use crate::{Color32, textures::TextureOptions};
|
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.
|
/// How to convert font coverage values into alpha and color values.
|
||||||
//
|
///
|
||||||
// This whole thing is less than rigorous.
|
/// epaint stores all glyphs in the font atlas as white (with varying opacity),
|
||||||
// Ideally we should do this in a shader instead, and use different computations
|
/// so that egui can reuse the same glyph for different text colors
|
||||||
// for different text colors.
|
/// (with a simple color multiplication in the shader).
|
||||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
///
|
||||||
|
/// 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)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum AlphaFromCoverage {
|
pub enum FontColorTransferFunction {
|
||||||
/// `alpha = coverage`.
|
/// 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.
|
/// This mode looks good for black-on-white text, i.e. light mode.
|
||||||
Linear,
|
Off,
|
||||||
|
|
||||||
/// `alpha = coverage^gamma`.
|
/// `alpha = coverage^gamma`.
|
||||||
|
///
|
||||||
|
/// Gamma=1 looks good for black-on-white text, i.e. light mode.
|
||||||
Gamma(f32),
|
Gamma(f32),
|
||||||
|
|
||||||
/// `alpha = 2 * coverage - coverage^2`
|
/// `alpha = 2 * coverage - coverage^2`
|
||||||
@@ -374,29 +390,59 @@ pub enum AlphaFromCoverage {
|
|||||||
TwoCoverageMinusCoverageSq,
|
TwoCoverageMinusCoverageSq,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlphaFromCoverage {
|
impl FontColorTransferFunction {
|
||||||
/// A good-looking default for light mode (black-on-white text).
|
/// 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).
|
/// A good-looking default for dark mode (white-on-black text).
|
||||||
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
|
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.
|
/// Convert coverage to alpha.
|
||||||
#[inline(always)]
|
#[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);
|
let coverage = coverage.clamp(0.0, 1.0);
|
||||||
match self {
|
match self {
|
||||||
Self::Linear => coverage,
|
Self::Off | Self::Gamma(1.0) => coverage,
|
||||||
Self::Gamma(gamma) => coverage.powf(*gamma),
|
Self::Gamma(gamma) => coverage.powf(gamma),
|
||||||
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[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);
|
let alpha = self.alpha_from_coverage(coverage);
|
||||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub use self::{
|
|||||||
corner_radius::CornerRadius,
|
corner_radius::CornerRadius,
|
||||||
corner_radius_f32::CornerRadiusF32,
|
corner_radius_f32::CornerRadiusF32,
|
||||||
direction::Direction,
|
direction::Direction,
|
||||||
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
|
image::{ColorImage, FontColorTransferFunction, ImageData, ImageDelta},
|
||||||
margin::Margin,
|
margin::Margin,
|
||||||
margin_f32::*,
|
margin_f32::*,
|
||||||
mesh::{Mesh, Mesh16, Vertex},
|
mesh::{Mesh, Mesh16, Vertex},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![expect(clippy::mem_forget)]
|
#![expect(clippy::mem_forget)]
|
||||||
|
|
||||||
|
use ecolor::Color32;
|
||||||
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
|
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
|
||||||
use self_cell::self_cell;
|
use self_cell::self_cell;
|
||||||
use skrifa::{GlyphId, MetadataProvider as _};
|
use skrifa::{GlyphId, MetadataProvider as _};
|
||||||
@@ -274,26 +275,31 @@ impl FontCell {
|
|||||||
let width = bounds.width() as u16;
|
let width = bounds.width() as u16;
|
||||||
let height = bounds.height() 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 {
|
let uv_rect = if width == 0 || height == 0 {
|
||||||
UvRect::default()
|
UvRect::default()
|
||||||
} else {
|
} 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 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 (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
|
||||||
let pixels = dest.data_as_u8_slice();
|
let pixels = dest.data_as_u8_slice();
|
||||||
for y in 0..height as usize {
|
for y in 0..height as usize {
|
||||||
for x in 0..width as usize {
|
for x in 0..width as usize {
|
||||||
image[(x + glyph_pos.0, y + glyph_pos.1)] = alpha_from_coverage
|
let pixel_offset = 4 * ((y * width as usize) + x);
|
||||||
.color_from_coverage(
|
image[(x + glyph_pos.0, y + glyph_pos.1)] = color_transfer_function
|
||||||
pixels[((y * width as usize) + x) * 4 + 3] as f32 / 255.0,
|
.to_atlas_color(Color32::from_rgba_premultiplied(
|
||||||
);
|
pixels[pixel_offset],
|
||||||
|
pixels[pixel_offset + 1],
|
||||||
|
pixels[pixel_offset + 2],
|
||||||
|
pixels[pixel_offset + 3],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
glyph_pos
|
glyph_pos
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ pub struct TextOptions {
|
|||||||
/// Maximum size of the font texture.
|
/// Maximum size of the font texture.
|
||||||
pub max_texture_side: usize,
|
pub max_texture_side: usize,
|
||||||
|
|
||||||
/// Controls how to convert glyph coverage to alpha.
|
/// Controls how to convert glyph colors when writing to the font atlas.
|
||||||
pub alpha_from_coverage: crate::AlphaFromCoverage,
|
pub color_transfer_function: crate::FontColorTransferFunction,
|
||||||
|
|
||||||
/// Whether to enable font hinting
|
/// Whether to enable font hinting
|
||||||
///
|
///
|
||||||
@@ -54,7 +54,7 @@ impl Default for TextOptions {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_texture_side: 2048, // Small but portable
|
max_texture_side: 2048, // Small but portable
|
||||||
alpha_from_coverage: crate::AlphaFromCoverage::default(),
|
color_transfer_function: crate::FontColorTransferFunction::default(),
|
||||||
font_hinting: true,
|
font_hinting: true,
|
||||||
subpixel_binning: true,
|
subpixel_binning: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,9 @@ impl TextureAtlas {
|
|||||||
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
||||||
let coverage =
|
let coverage =
|
||||||
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
|
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)] =
|
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = options
|
||||||
options.alpha_from_coverage.color_from_coverage(coverage);
|
.color_transfer_function
|
||||||
|
.color_from_coverage(coverage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
atlas.discs.push(PrerasterizedDisc {
|
atlas.discs.push(PrerasterizedDisc {
|
||||||
|
|||||||
Reference in New Issue
Block a user