Use premultiplied alpha for all colors + improve painting of thin lines
This commit is contained in:
parent
4fcea59929
commit
3860807e29
6 changed files with 130 additions and 48 deletions
|
@ -48,6 +48,9 @@ Features:
|
||||||
* Simple text input
|
* Simple text input
|
||||||
* Anti-aliased rendering of circles, rounded rectangles and lines.
|
* Anti-aliased rendering of circles, rounded rectangles and lines.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
* All coordinates are screen space coordinates, in locial "points" (which may consist of many physical pixels).
|
||||||
|
* All colors have premultiplied alpha
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code from something I hated to do to something I now like to do.
|
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code from something I hated to do to something I now like to do.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// 0-255 `sRGBA`. TODO: rename `sRGBA` for clarity.
|
/// 0-255 `sRGBA`. TODO: rename `sRGBA` for clarity.
|
||||||
|
/// Uses premultiplied alpha.
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
pub r: u8,
|
pub r: u8,
|
||||||
|
@ -9,17 +10,6 @@ pub struct Color {
|
||||||
pub a: u8,
|
pub a: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
|
||||||
pub fn transparent(self) -> Color {
|
|
||||||
Color {
|
|
||||||
r: self.r,
|
|
||||||
g: self.g,
|
|
||||||
b: self.b,
|
|
||||||
a: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Color {
|
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Color {
|
||||||
Color { r, g, b, a }
|
Color { r, g, b, a }
|
||||||
}
|
}
|
||||||
|
@ -33,15 +23,34 @@ pub const fn gray(l: u8, a: u8) -> Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn white(a: u8) -> Color {
|
pub const fn black(a: u8) -> Color {
|
||||||
Color {
|
Color {
|
||||||
r: 255,
|
r: 0,
|
||||||
g: 255,
|
g: 0,
|
||||||
b: 255,
|
b: 0,
|
||||||
a,
|
a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn white(a: u8) -> Color {
|
||||||
|
Color {
|
||||||
|
r: a,
|
||||||
|
g: a,
|
||||||
|
b: a,
|
||||||
|
a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn additive_gray(l: u8) -> Color {
|
||||||
|
Color {
|
||||||
|
r: l,
|
||||||
|
g: l,
|
||||||
|
b: l,
|
||||||
|
a: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TRANSPARENT: Color = srgba(0, 0, 0, 0);
|
||||||
pub const BLACK: Color = srgba(0, 0, 0, 255);
|
pub const BLACK: Color = srgba(0, 0, 0, 255);
|
||||||
pub const LIGHT_GRAY: Color = srgba(220, 220, 220, 255);
|
pub const LIGHT_GRAY: Color = srgba(220, 220, 220, 255);
|
||||||
pub const WHITE: Color = srgba(255, 255, 255, 255);
|
pub const WHITE: Color = srgba(255, 255, 255, 255);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/// Outputs render info in a format suitable for e.g. OpenGL.
|
/// Outputs render info in a format suitable for e.g. OpenGL.
|
||||||
use crate::{
|
use crate::{
|
||||||
color::{srgba, Color},
|
color::{self, srgba, Color},
|
||||||
fonts::Fonts,
|
fonts::Fonts,
|
||||||
math::*,
|
math::*,
|
||||||
types::PaintCmd,
|
types::PaintCmd,
|
||||||
|
@ -17,7 +17,7 @@ pub struct Vertex {
|
||||||
pub pos: Pos2,
|
pub pos: Pos2,
|
||||||
/// Texel indices into the texture
|
/// Texel indices into the texture
|
||||||
pub uv: (u16, u16),
|
pub uv: (u16, u16),
|
||||||
/// sRGBA
|
/// sRGBA, premultiplied alpha
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +255,7 @@ use self::PathType::{Closed, Open};
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct MesherOptions {
|
pub struct MesherOptions {
|
||||||
pub anti_alias: bool,
|
pub anti_alias: bool,
|
||||||
|
/// Size of a pixel in points, e.g. 0.5
|
||||||
pub aa_size: f32,
|
pub aa_size: f32,
|
||||||
pub debug_paint_clip_rects: bool,
|
pub debug_paint_clip_rects: bool,
|
||||||
}
|
}
|
||||||
|
@ -270,6 +271,10 @@ impl Default for MesherOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_closed_path(mesh: &mut Mesh, options: MesherOptions, path: &[PathPoint], color: Color) {
|
pub fn fill_closed_path(mesh: &mut Mesh, options: MesherOptions, path: &[PathPoint], color: Color) {
|
||||||
|
if color == color::TRANSPARENT {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let n = path.len() as u32;
|
let n = path.len() as u32;
|
||||||
let vert = |pos, color| Vertex {
|
let vert = |pos, color| Vertex {
|
||||||
pos,
|
pos,
|
||||||
|
@ -277,7 +282,7 @@ pub fn fill_closed_path(mesh: &mut Mesh, options: MesherOptions, path: &[PathPoi
|
||||||
color,
|
color,
|
||||||
};
|
};
|
||||||
if options.anti_alias {
|
if options.anti_alias {
|
||||||
let color_outer = color.transparent();
|
let color_outer = color::TRANSPARENT;
|
||||||
let idx_inner = mesh.vertices.len() as u32;
|
let idx_inner = mesh.vertices.len() as u32;
|
||||||
let idx_outer = idx_inner + 1;
|
let idx_outer = idx_inner + 1;
|
||||||
for i in 2..n {
|
for i in 2..n {
|
||||||
|
@ -311,8 +316,11 @@ pub fn paint_path(
|
||||||
color: Color,
|
color: Color,
|
||||||
width: f32,
|
width: f32,
|
||||||
) {
|
) {
|
||||||
|
if color == color::TRANSPARENT {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let n = path.len() as u32;
|
let n = path.len() as u32;
|
||||||
let hw = width / 2.0;
|
|
||||||
let idx = mesh.vertices.len() as u32;
|
let idx = mesh.vertices.len() as u32;
|
||||||
|
|
||||||
let vert = |pos, color| Vertex {
|
let vert = |pos, color| Vertex {
|
||||||
|
@ -322,18 +330,27 @@ pub fn paint_path(
|
||||||
};
|
};
|
||||||
|
|
||||||
if options.anti_alias {
|
if options.anti_alias {
|
||||||
let color_outer = color.transparent();
|
let color_inner = color;
|
||||||
let thin_line = width <= 1.0;
|
let color_outer = color::TRANSPARENT;
|
||||||
let mut color_inner = color;
|
|
||||||
|
let thin_line = width <= options.aa_size;
|
||||||
if thin_line {
|
if thin_line {
|
||||||
|
/*
|
||||||
|
We paint the line using three edges: outer, inner, outer.
|
||||||
|
|
||||||
|
. o i o outer, inner, outer
|
||||||
|
. |---| aa_size (pixel width)
|
||||||
|
*/
|
||||||
|
|
||||||
// Fade out as it gets thinner:
|
// Fade out as it gets thinner:
|
||||||
color_inner.a = (f32::from(color_inner.a) * width).round() as u8;
|
let color_inner = mul_color(color_inner, width / options.aa_size);
|
||||||
}
|
if color_inner == color::TRANSPARENT {
|
||||||
// TODO: line caps ?
|
return;
|
||||||
let mut i0 = n - 1;
|
}
|
||||||
for i1 in 0..n {
|
|
||||||
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
let mut i0 = n - 1;
|
||||||
if thin_line {
|
for i1 in 0..n {
|
||||||
|
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||||
let p1 = &path[i1 as usize];
|
let p1 = &path[i1 as usize];
|
||||||
let p = p1.pos;
|
let p = p1.pos;
|
||||||
let n = p1.normal;
|
let n = p1.normal;
|
||||||
|
@ -350,17 +367,33 @@ pub fn paint_path(
|
||||||
mesh.triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
|
mesh.triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
|
||||||
mesh.triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
|
mesh.triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
|
||||||
}
|
}
|
||||||
} else {
|
i0 = i1;
|
||||||
let hw = (width - options.aa_size) * 0.5;
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: line caps for really thick lines?
|
||||||
|
|
||||||
|
/*
|
||||||
|
We paint the line using four edges: outer, inner, inner, outer
|
||||||
|
|
||||||
|
. o i p i o outer, inner, point, inner, outer
|
||||||
|
. |---| aa_size (pixel width)
|
||||||
|
. |--------------| width
|
||||||
|
. |---------| outer_rad
|
||||||
|
. |-----| inner_rad
|
||||||
|
*/
|
||||||
|
|
||||||
|
let mut i0 = n - 1;
|
||||||
|
for i1 in 0..n {
|
||||||
|
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||||
|
let inner_rad = 0.5 * (width - options.aa_size);
|
||||||
|
let outer_rad = 0.5 * (width + options.aa_size);
|
||||||
let p1 = &path[i1 as usize];
|
let p1 = &path[i1 as usize];
|
||||||
let p = p1.pos;
|
let p = p1.pos;
|
||||||
let n = p1.normal;
|
let n = p1.normal;
|
||||||
mesh.vertices
|
mesh.vertices.push(vert(p + n * outer_rad, color_outer));
|
||||||
.push(vert(p + n * (hw + options.aa_size), color_outer));
|
mesh.vertices.push(vert(p + n * inner_rad, color_inner));
|
||||||
mesh.vertices.push(vert(p + n * (hw + 0.0), color_inner));
|
mesh.vertices.push(vert(p - n * inner_rad, color_inner));
|
||||||
mesh.vertices.push(vert(p - n * (hw + 0.0), color_inner));
|
mesh.vertices.push(vert(p - n * outer_rad, color_outer));
|
||||||
mesh.vertices
|
|
||||||
.push(vert(p - n * (hw + options.aa_size), color_outer));
|
|
||||||
|
|
||||||
if connect_with_previous {
|
if connect_with_previous {
|
||||||
mesh.triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
|
mesh.triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
|
||||||
|
@ -372,8 +405,8 @@ pub fn paint_path(
|
||||||
mesh.triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2);
|
mesh.triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2);
|
||||||
mesh.triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3);
|
mesh.triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3);
|
||||||
}
|
}
|
||||||
|
i0 = i1;
|
||||||
}
|
}
|
||||||
i0 = i1;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let last_index = if path_type == Closed { n } else { n - 1 };
|
let last_index = if path_type == Closed { n } else { n - 1 };
|
||||||
|
@ -390,13 +423,39 @@ pub fn paint_path(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for p in path {
|
let thin_line = width <= options.aa_size;
|
||||||
mesh.vertices.push(vert(p.pos + hw * p.normal, color));
|
if thin_line {
|
||||||
mesh.vertices.push(vert(p.pos - hw * p.normal, color));
|
// Fade out thin lines rather than making them thinner
|
||||||
|
let radius = options.aa_size / 2.0;
|
||||||
|
let color = mul_color(color, width / options.aa_size);
|
||||||
|
if color == color::TRANSPARENT {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for p in path {
|
||||||
|
mesh.vertices.push(vert(p.pos + radius * p.normal, color));
|
||||||
|
mesh.vertices.push(vert(p.pos - radius * p.normal, color));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let radius = width / 2.0;
|
||||||
|
for p in path {
|
||||||
|
mesh.vertices.push(vert(p.pos + radius * p.normal, color));
|
||||||
|
mesh.vertices.push(vert(p.pos - radius * p.normal, color));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mul_color(color: Color, factor: f32) -> Color {
|
||||||
|
// TODO: sRGBA correct fading
|
||||||
|
debug_assert!(0.0 <= factor && factor <= 1.0);
|
||||||
|
Color {
|
||||||
|
r: (f32::from(color.r) * factor).round() as u8,
|
||||||
|
g: (f32::from(color.g) * factor).round() as u8,
|
||||||
|
b: (f32::from(color.b) * factor).round() as u8,
|
||||||
|
a: (f32::from(color.a) * factor).round() as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// path: only used to reuse memory
|
/// path: only used to reuse memory
|
||||||
|
|
|
@ -210,7 +210,7 @@ impl Style {
|
||||||
ui.add(Slider::f32(&mut self.button_padding.y, 0.0..=20.0).text("button_padding.y").precision(0));
|
ui.add(Slider::f32(&mut self.button_padding.y, 0.0..=20.0).text("button_padding.y").precision(0));
|
||||||
ui.add(Slider::f32(&mut self.clickable_diameter, 0.0..=60.0).text("clickable_diameter").precision(0));
|
ui.add(Slider::f32(&mut self.clickable_diameter, 0.0..=60.0).text("clickable_diameter").precision(0));
|
||||||
ui.add(Slider::f32(&mut self.start_icon_width, 0.0..=60.0).text("start_icon_width").precision(0));
|
ui.add(Slider::f32(&mut self.start_icon_width, 0.0..=60.0).text("start_icon_width").precision(0));
|
||||||
ui.add(Slider::f32(&mut self.line_width, 0.0..=10.0).text("line_width").precision(0));
|
ui.add(Slider::f32(&mut self.line_width, 0.0..=10.0).text("line_width").precision(1));
|
||||||
ui.add(Slider::f32(&mut self.animation_time, 0.0..=1.0).text("animation_time").precision(2));
|
ui.add(Slider::f32(&mut self.animation_time, 0.0..=1.0).text("animation_time").precision(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ impl Painter {
|
||||||
if (v_pos.y > v_clip_rect.w) { discard; }
|
if (v_pos.y > v_clip_rect.w) { discard; }
|
||||||
f_color = v_color;
|
f_color = v_color;
|
||||||
f_color.rgb = linear_from_srgb(f_color.rgb);
|
f_color.rgb = linear_from_srgb(f_color.rgb);
|
||||||
f_color.a *= texture(u_sampler, v_tc).r;
|
f_color *= texture(u_sampler, v_tc).r;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
},
|
},
|
||||||
|
@ -118,7 +118,7 @@ impl Painter {
|
||||||
if (v_pos.y > v_clip_rect.w) { discard; }
|
if (v_pos.y > v_clip_rect.w) { discard; }
|
||||||
gl_FragColor = v_color;
|
gl_FragColor = v_color;
|
||||||
gl_FragColor.rgb = linear_from_srgb(gl_FragColor.rgb);
|
gl_FragColor.rgb = linear_from_srgb(gl_FragColor.rgb);
|
||||||
gl_FragColor.a *= texture2D(u_sampler, v_tc).r;
|
gl_FragColor *= texture2D(u_sampler, v_tc).r;
|
||||||
}
|
}
|
||||||
",
|
",
|
||||||
},
|
},
|
||||||
|
@ -172,7 +172,7 @@ impl Painter {
|
||||||
if (v_pos.y > v_clip_rect.w) { discard; }
|
if (v_pos.y > v_clip_rect.w) { discard; }
|
||||||
gl_FragColor = v_color;
|
gl_FragColor = v_color;
|
||||||
gl_FragColor.rgb = linear_from_srgb(gl_FragColor.rgb);
|
gl_FragColor.rgb = linear_from_srgb(gl_FragColor.rgb);
|
||||||
gl_FragColor.a *= texture2D(u_sampler, v_tc).r;
|
gl_FragColor *= texture2D(u_sampler, v_tc).r;
|
||||||
}
|
}
|
||||||
",
|
",
|
||||||
},
|
},
|
||||||
|
@ -274,8 +274,19 @@ impl Painter {
|
||||||
u_sampler: &self.texture,
|
u_sampler: &self.texture,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Emilib outputs colors with premultiplied alpha:
|
||||||
|
let blend_func = glium::BlendingFunction::Addition {
|
||||||
|
source: glium::LinearBlendingFactor::One,
|
||||||
|
destination: glium::LinearBlendingFactor::OneMinusSourceAlpha,
|
||||||
|
};
|
||||||
|
let blend = glium::Blend {
|
||||||
|
color: blend_func,
|
||||||
|
alpha: blend_func,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let params = glium::DrawParameters {
|
let params = glium::DrawParameters {
|
||||||
blend: glium::Blend::alpha_blending(),
|
blend,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl Painter {
|
||||||
if (v_pos.x > v_clip_rect.z) { discard; }
|
if (v_pos.x > v_clip_rect.z) { discard; }
|
||||||
if (v_pos.y > v_clip_rect.w) { discard; }
|
if (v_pos.y > v_clip_rect.w) { discard; }
|
||||||
gl_FragColor = v_color;
|
gl_FragColor = v_color;
|
||||||
gl_FragColor.a *= texture2D(u_sampler, v_tc).a;
|
gl_FragColor *= texture2D(u_sampler, v_tc).a;
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
@ -163,7 +163,7 @@ impl Painter {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
gl.enable(Gl::BLEND);
|
gl.enable(Gl::BLEND);
|
||||||
gl.blend_func(Gl::SRC_ALPHA, Gl::ONE_MINUS_SRC_ALPHA);
|
gl.blend_func(Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA); // premultiplied alpha
|
||||||
gl.use_program(Some(&self.program));
|
gl.use_program(Some(&self.program));
|
||||||
gl.active_texture(Gl::TEXTURE0);
|
gl.active_texture(Gl::TEXTURE0);
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture));
|
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture));
|
||||||
|
|
Loading…
Reference in a new issue