diff --git a/README.md b/README.md index 59b19d2e..a7b07773 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ Features: * Simple text input * 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 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. diff --git a/emigui/src/color.rs b/emigui/src/color.rs index e123a918..c3c673a4 100644 --- a/emigui/src/color.rs +++ b/emigui/src/color.rs @@ -1,6 +1,7 @@ use serde_derive::{Deserialize, Serialize}; /// 0-255 `sRGBA`. TODO: rename `sRGBA` for clarity. +/// Uses premultiplied alpha. #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] pub struct Color { pub r: u8, @@ -9,17 +10,6 @@ pub struct Color { 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 { 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 { - r: 255, - g: 255, - b: 255, + r: 0, + g: 0, + b: 0, 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 LIGHT_GRAY: Color = srgba(220, 220, 220, 255); pub const WHITE: Color = srgba(255, 255, 255, 255); diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index 3175c028..fe5d911d 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -2,7 +2,7 @@ /// Outputs render info in a format suitable for e.g. OpenGL. use crate::{ - color::{srgba, Color}, + color::{self, srgba, Color}, fonts::Fonts, math::*, types::PaintCmd, @@ -17,7 +17,7 @@ pub struct Vertex { pub pos: Pos2, /// Texel indices into the texture pub uv: (u16, u16), - /// sRGBA + /// sRGBA, premultiplied alpha pub color: Color, } @@ -255,6 +255,7 @@ use self::PathType::{Closed, Open}; #[derive(Clone, Copy)] pub struct MesherOptions { pub anti_alias: bool, + /// Size of a pixel in points, e.g. 0.5 pub aa_size: f32, 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) { + if color == color::TRANSPARENT { + return; + } + let n = path.len() as u32; let vert = |pos, color| Vertex { pos, @@ -277,7 +282,7 @@ pub fn fill_closed_path(mesh: &mut Mesh, options: MesherOptions, path: &[PathPoi color, }; if options.anti_alias { - let color_outer = color.transparent(); + let color_outer = color::TRANSPARENT; let idx_inner = mesh.vertices.len() as u32; let idx_outer = idx_inner + 1; for i in 2..n { @@ -311,8 +316,11 @@ pub fn paint_path( color: Color, width: f32, ) { + if color == color::TRANSPARENT { + return; + } + let n = path.len() as u32; - let hw = width / 2.0; let idx = mesh.vertices.len() as u32; let vert = |pos, color| Vertex { @@ -322,18 +330,27 @@ pub fn paint_path( }; if options.anti_alias { - let color_outer = color.transparent(); - let thin_line = width <= 1.0; - let mut color_inner = color; + let color_inner = color; + let color_outer = color::TRANSPARENT; + + let thin_line = width <= options.aa_size; 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: - color_inner.a = (f32::from(color_inner.a) * width).round() as u8; - } - // TODO: line caps ? - let mut i0 = n - 1; - for i1 in 0..n { - let connect_with_previous = path_type == PathType::Closed || i1 > 0; - if thin_line { + let color_inner = mul_color(color_inner, width / options.aa_size); + if color_inner == color::TRANSPARENT { + return; + } + + let mut i0 = n - 1; + for i1 in 0..n { + let connect_with_previous = path_type == PathType::Closed || i1 > 0; let p1 = &path[i1 as usize]; let p = p1.pos; 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 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2); } - } else { - let hw = (width - options.aa_size) * 0.5; + i0 = i1; + } + } 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 p = p1.pos; let n = p1.normal; - mesh.vertices - .push(vert(p + n * (hw + options.aa_size), color_outer)); - mesh.vertices.push(vert(p + n * (hw + 0.0), color_inner)); - mesh.vertices.push(vert(p - n * (hw + 0.0), color_inner)); - mesh.vertices - .push(vert(p - n * (hw + options.aa_size), color_outer)); + mesh.vertices.push(vert(p + n * outer_rad, color_outer)); + mesh.vertices.push(vert(p + n * inner_rad, color_inner)); + mesh.vertices.push(vert(p - n * inner_rad, color_inner)); + mesh.vertices.push(vert(p - n * outer_rad, color_outer)); if connect_with_previous { 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 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3); } + i0 = i1; } - i0 = i1; } } else { let last_index = if path_type == Closed { n } else { n - 1 }; @@ -390,13 +423,39 @@ pub fn paint_path( ); } - for p in path { - mesh.vertices.push(vert(p.pos + hw * p.normal, color)); - mesh.vertices.push(vert(p.pos - hw * p.normal, color)); + let thin_line = width <= options.aa_size; + if thin_line { + // 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 diff --git a/emigui/src/style.rs b/emigui/src/style.rs index d38d59a9..294f5a9b 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -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.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.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)); } } diff --git a/emigui_glium/src/painter.rs b/emigui_glium/src/painter.rs index b431c410..cd1f56cf 100644 --- a/emigui_glium/src/painter.rs +++ b/emigui_glium/src/painter.rs @@ -64,7 +64,7 @@ impl Painter { if (v_pos.y > v_clip_rect.w) { discard; } f_color = v_color; 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; } gl_FragColor = v_color; 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; } gl_FragColor = v_color; 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, }; + // 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 { - blend: glium::Blend::alpha_blending(), + blend, ..Default::default() }; diff --git a/emigui_wasm/src/webgl.rs b/emigui_wasm/src/webgl.rs index 986d030a..f335370a 100644 --- a/emigui_wasm/src/webgl.rs +++ b/emigui_wasm/src/webgl.rs @@ -97,7 +97,7 @@ impl Painter { if (v_pos.x > v_clip_rect.z) { discard; } if (v_pos.y > v_clip_rect.w) { discard; } 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; 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.active_texture(Gl::TEXTURE0); gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture));