diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index 396bbfa76..3c11cce33 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -1697,20 +1697,47 @@ pub(crate) fn wheel_pick(v: egui::Vec2, rad: f32, lock: Option) -> (f } fn paint_wheel(p: &egui::Painter, c: Pos2, rad: f32) { + p.add(wheel_mesh(c, rad)); + p.circle_stroke(c, rad, Stroke::new(1.0, Color32::from_gray(40))); +} + +/// The picture of a grading wheel: a hue disc whose rim is fully saturated display sRGB at full +/// brightness, fading to a neutral grey at the centre. It is built from concentric rings so the +/// saturation ramps evenly with the radius instead of one fan of triangles blending each hue +/// straight into grey (which made the whole disc look muddy, issue #77). Only the drawing: +/// what a position means for the edit is [`wheel_pick`]. +pub(crate) fn wheel_mesh(c: Pos2, rad: f32) -> Mesh { + const SEGMENTS: u32 = 72; + const RINGS: u32 = 12; + /// Brightness (HSV value) at the centre; the rim is 1.0. + const CENTRE_V: f32 = 0.6; + let colour = |hue_deg: f32, t: f32| { + let rgb = lightcraft_color::perceptual::hsv_to_rgb(hue_deg, t, CENTRE_V + (1.0 - CENTRE_V) * t); + let u = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; + Color32::from_rgb(u(rgb[0]), u(rgb[1]), u(rgb[2])) + }; let mut mesh = Mesh::default(); - let n = 48; - mesh.vertices.push(Vertex { pos: c, uv: Pos2::ZERO, color: Color32::from_gray(128) }); - for i in 0..=n { - let a = i as f32 / n as f32 * std::f32::consts::TAU; - let rgb = lightcraft_color::perceptual::hsv_to_rgb(a.to_degrees(), 0.75, 0.8); - let col = Color32::from_rgb((rgb[0] * 255.0) as u8, (rgb[1] * 255.0) as u8, (rgb[2] * 255.0) as u8); - mesh.vertices.push(Vertex { pos: c + vec2(a.cos(), -a.sin()) * rad, uv: Pos2::ZERO, color: col }); - if i > 0 { - mesh.indices.extend([0, i as u32, i as u32 + 1]); + mesh.vertices.push(Vertex { pos: c, uv: Pos2::ZERO, color: colour(0.0, 0.0) }); + // ring k (1..=RINGS) holds SEGMENTS + 1 vertices starting at index 1 + (k - 1) * (SEGMENTS + 1) + let row = SEGMENTS + 1; + for k in 1..=RINGS { + let t = k as f32 / RINGS as f32; + for i in 0..=SEGMENTS { + let a = i as f32 / SEGMENTS as f32 * std::f32::consts::TAU; + mesh.vertices.push(Vertex { pos: c + vec2(a.cos(), -a.sin()) * rad * t, uv: Pos2::ZERO, color: colour(a.to_degrees(), t) }); } } - p.add(mesh); - p.circle_stroke(c, rad, Stroke::new(1.0, Color32::from_gray(40))); + for i in 0..SEGMENTS { + mesh.indices.extend([0, 1 + i, 2 + i]); + } + for k in 1..RINGS { + let inner = 1 + (k - 1) * row; + let outer = inner + row; + for i in 0..SEGMENTS { + mesh.indices.extend([inner + i, outer + i, outer + i + 1, inner + i, outer + i + 1, inner + i + 1]); + } + } + mesh } /// Quick Develop (grid with several photos selected): relative steps applied to every selected diff --git a/crates/ui-egui/src/tests_grading.rs b/crates/ui-egui/src/tests_grading.rs index 6abce5460..f6903550c 100644 --- a/crates/ui-egui/src/tests_grading.rs +++ b/crates/ui-egui/src/tests_grading.rs @@ -352,3 +352,26 @@ fn double_click_still_resets_the_wheel_with_a_modifier_held() { h.step(); assert_eq!(wheel_value(&h, "midtones"), (0.0, 0.0)); } + +/// The wheels used to look muddy. Painted on screen, the rim of each hue reaches full +/// saturation at normal brightness and the centre is neutral. +#[test] +fn wheel_rim_is_fully_saturated_and_the_centre_neutral() { + let mut h = demo(); + let img = h.snapshot(SETTLE); + let r = widget(&h, "wheel:midtones").unwrap(); + let at = |dx: f32, dy: f32| { + let p = r.center() + egui::vec2(dx, dy) * r.width() / 2.0; + img[(p.x as usize, p.y as usize)] + }; + for deg in [0.0f32, 60.0, 120.0, 180.0, 240.0, 300.0] { + let a = deg.to_radians(); + let p = at(a.cos() * 0.93, -a.sin() * 0.93); + let (hi, lo) = (p.r().max(p.g()).max(p.b()), p.r().min(p.g()).min(p.b())); + assert!(hi >= 225 && lo <= 40, "rim at {deg}° is not a vivid hue: {p:?}"); + } + // just off the centre (the puck sits on it at zero), the disc is still close to neutral + let p = at(0.12, 0.12); + let (hi, lo) = (p.r().max(p.g()).max(p.b()), p.r().min(p.g()).min(p.b())); + assert!(hi - lo <= 40 && lo >= 100, "centre is not a neutral grey: {p:?}"); +}