
* Add blank lines above all `fn`, `impl`, `struct`, etc * Even newlines between docstringed struct and enum fields * Improve some documentation
309 lines
8.9 KiB
Rust
309 lines
8.9 KiB
Rust
use crate::{textures::TextureFilter, Color32};
|
|
|
|
/// An image stored in RAM.
|
|
///
|
|
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
|
|
///
|
|
/// In order to paint the image on screen, you first need to convert it to
|
|
///
|
|
/// See also: [`ColorImage`], [`FontImage`].
|
|
#[derive(Clone, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub enum ImageData {
|
|
/// RGBA image.
|
|
Color(ColorImage),
|
|
|
|
/// Used for the font texture.
|
|
Font(FontImage),
|
|
}
|
|
|
|
impl ImageData {
|
|
pub fn size(&self) -> [usize; 2] {
|
|
match self {
|
|
Self::Color(image) => image.size,
|
|
Self::Font(image) => image.size,
|
|
}
|
|
}
|
|
|
|
pub fn width(&self) -> usize {
|
|
self.size()[0]
|
|
}
|
|
|
|
pub fn height(&self) -> usize {
|
|
self.size()[1]
|
|
}
|
|
|
|
pub fn bytes_per_pixel(&self) -> usize {
|
|
match self {
|
|
Self::Color(_) | Self::Font(_) => 4,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// A 2D RGBA color image in RAM.
|
|
#[derive(Clone, Default, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct ColorImage {
|
|
/// width, height.
|
|
pub size: [usize; 2],
|
|
|
|
/// The pixels, row by row, from top to bottom.
|
|
pub pixels: Vec<Color32>,
|
|
}
|
|
|
|
impl ColorImage {
|
|
/// Create an image filled with the given color.
|
|
pub fn new(size: [usize; 2], color: Color32) -> Self {
|
|
Self {
|
|
size,
|
|
pixels: vec![color; size[0] * size[1]],
|
|
}
|
|
}
|
|
|
|
/// Create a [`ColorImage`] from flat un-multiplied RGBA data.
|
|
///
|
|
/// This is usually what you want to use after having loaded an image file.
|
|
///
|
|
/// Panics if `size[0] * size[1] * 4 != rgba.len()`.
|
|
///
|
|
/// ## Example using the [`image`](crates.io/crates/image) crate:
|
|
/// ``` ignore
|
|
/// fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> {
|
|
/// let image = image::io::Reader::open(path)?.decode()?;
|
|
/// let size = [image.width() as _, image.height() as _];
|
|
/// let image_buffer = image.to_rgba8();
|
|
/// let pixels = image_buffer.as_flat_samples();
|
|
/// Ok(egui::ColorImage::from_rgba_unmultiplied(
|
|
/// size,
|
|
/// pixels.as_slice(),
|
|
/// ))
|
|
/// }
|
|
///
|
|
/// fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
|
|
/// let image = image::load_from_memory(image_data)?;
|
|
/// let size = [image.width() as _, image.height() as _];
|
|
/// let image_buffer = image.to_rgba8();
|
|
/// let pixels = image_buffer.as_flat_samples();
|
|
/// Ok(ColorImage::from_rgba_unmultiplied(
|
|
/// size,
|
|
/// pixels.as_slice(),
|
|
/// ))
|
|
/// }
|
|
/// ```
|
|
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
|
|
assert_eq!(size[0] * size[1] * 4, rgba.len());
|
|
let pixels = rgba
|
|
.chunks_exact(4)
|
|
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
|
.collect();
|
|
Self { size, pixels }
|
|
}
|
|
|
|
/// An example color image, useful for tests.
|
|
pub fn example() -> Self {
|
|
let width = 128;
|
|
let height = 64;
|
|
let mut img = Self::new([width, height], Color32::TRANSPARENT);
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
let h = x as f32 / width as f32;
|
|
let s = 1.0;
|
|
let v = 1.0;
|
|
let a = y as f32 / height as f32;
|
|
img[(x, y)] = crate::color::Hsva { h, s, v, a }.into();
|
|
}
|
|
}
|
|
img
|
|
}
|
|
|
|
#[inline]
|
|
pub fn width(&self) -> usize {
|
|
self.size[0]
|
|
}
|
|
|
|
#[inline]
|
|
pub fn height(&self) -> usize {
|
|
self.size[1]
|
|
}
|
|
}
|
|
|
|
impl std::ops::Index<(usize, usize)> for ColorImage {
|
|
type Output = Color32;
|
|
|
|
#[inline]
|
|
fn index(&self, (x, y): (usize, usize)) -> &Color32 {
|
|
let [w, h] = self.size;
|
|
assert!(x < w && y < h);
|
|
&self.pixels[y * w + x]
|
|
}
|
|
}
|
|
|
|
impl std::ops::IndexMut<(usize, usize)> for ColorImage {
|
|
#[inline]
|
|
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 {
|
|
let [w, h] = self.size;
|
|
assert!(x < w && y < h);
|
|
&mut self.pixels[y * w + x]
|
|
}
|
|
}
|
|
|
|
impl From<ColorImage> for ImageData {
|
|
#[inline(always)]
|
|
fn from(image: ColorImage) -> Self {
|
|
Self::Color(image)
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// A single-channel image designed for the font texture.
|
|
///
|
|
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
|
///
|
|
/// This is roughly interpreted as the opacity of a white image.
|
|
#[derive(Clone, Default, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct FontImage {
|
|
/// width, height
|
|
pub size: [usize; 2],
|
|
|
|
/// The coverage value.
|
|
///
|
|
/// Often you want to use [`Self::srgba_pixels`] instead.
|
|
pub pixels: Vec<f32>,
|
|
}
|
|
|
|
impl FontImage {
|
|
pub fn new(size: [usize; 2]) -> Self {
|
|
Self {
|
|
size,
|
|
pixels: vec![0.0; size[0] * size[1]],
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn width(&self) -> usize {
|
|
self.size[0]
|
|
}
|
|
|
|
#[inline]
|
|
pub fn height(&self) -> usize {
|
|
self.size[1]
|
|
}
|
|
|
|
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
|
///
|
|
/// `gamma` should normally be set to 1.0.
|
|
/// If you are having problems with text looking skinny and pixelated, try
|
|
/// setting a lower gamma, e.g. `0.5`.
|
|
pub fn srgba_pixels(&'_ self, gamma: f32) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
|
self.pixels.iter().map(move |coverage| {
|
|
// This is arbitrarily chosen to make text look as good as possible.
|
|
// In particular, it looks good with gamma=1 and the default eframe backend,
|
|
// which uses linear blending.
|
|
// See https://github.com/emilk/egui/issues/1410
|
|
let a = fast_round(coverage.powf(gamma / 2.2) * 255.0);
|
|
Color32::from_rgba_premultiplied(a, a, a, a) // this makes no sense, but works
|
|
})
|
|
}
|
|
|
|
/// Clone a sub-region as a new image.
|
|
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> FontImage {
|
|
assert!(x + w <= self.width());
|
|
assert!(y + h <= self.height());
|
|
|
|
let mut pixels = Vec::with_capacity(w * h);
|
|
for y in y..y + h {
|
|
let offset = y * self.width() + x;
|
|
pixels.extend(&self.pixels[offset..(offset + w)]);
|
|
}
|
|
assert_eq!(pixels.len(), w * h);
|
|
FontImage {
|
|
size: [w, h],
|
|
pixels,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::ops::Index<(usize, usize)> for FontImage {
|
|
type Output = f32;
|
|
|
|
#[inline]
|
|
fn index(&self, (x, y): (usize, usize)) -> &f32 {
|
|
let [w, h] = self.size;
|
|
assert!(x < w && y < h);
|
|
&self.pixels[y * w + x]
|
|
}
|
|
}
|
|
|
|
impl std::ops::IndexMut<(usize, usize)> for FontImage {
|
|
#[inline]
|
|
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 {
|
|
let [w, h] = self.size;
|
|
assert!(x < w && y < h);
|
|
&mut self.pixels[y * w + x]
|
|
}
|
|
}
|
|
|
|
impl From<FontImage> for ImageData {
|
|
#[inline(always)]
|
|
fn from(image: FontImage) -> Self {
|
|
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.
|
|
///
|
|
/// Either a whole new image, or an update to a rectangular region of it.
|
|
#[derive(Clone, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
#[must_use = "The painter must take care of this"]
|
|
pub struct ImageDelta {
|
|
/// What to set the texture to.
|
|
///
|
|
/// If [`Self::pos`] is `None`, this describes the whole texture.
|
|
///
|
|
/// If [`Self::pos`] is `Some`, this describes a patch of the whole image starting at [`Self::pos`].
|
|
pub image: ImageData,
|
|
|
|
pub filter: TextureFilter,
|
|
|
|
/// If `None`, set the whole texture to [`Self::image`].
|
|
///
|
|
/// If `Some(pos)`, update a sub-region of an already allocated texture with the patch in [`Self::image`].
|
|
pub pos: Option<[usize; 2]>,
|
|
}
|
|
|
|
impl ImageDelta {
|
|
/// Update the whole texture.
|
|
pub fn full(image: impl Into<ImageData>, filter: TextureFilter) -> Self {
|
|
Self {
|
|
image: image.into(),
|
|
filter,
|
|
pos: None,
|
|
}
|
|
}
|
|
|
|
/// Update a sub-region of an existing texture.
|
|
pub fn partial(pos: [usize; 2], image: impl Into<ImageData>, filter: TextureFilter) -> Self {
|
|
Self {
|
|
image: image.into(),
|
|
filter,
|
|
pos: Some(pos),
|
|
}
|
|
}
|
|
|
|
/// Is this affecting the whole texture?
|
|
/// If `false`, this is a partial (sub-region) update.
|
|
pub fn is_whole(&self) -> bool {
|
|
self.pos.is_none()
|
|
}
|
|
}
|