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:
parent
d3c002a4e5
commit
8272b08742
11 changed files with 57 additions and 55 deletions
|
@ -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)).
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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::{
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue